8086 体系 · 国内标准教材

微机原理与接口技术 · 12 篇教程合集

从数制编码到 A/D 转换,一条线走完。每篇 30 分钟速通、6 站结构、自带自测与易错点,所有数字都实际核算过。

12 篇 · 60 张电路/时序图 · 约 650 KB 正文
单文件离线可用 · 支持深色模式 · 自动记住学习进度

第一部分 · 基础与 CPU

第二部分 · 存储器与指令系统

第三部分 · 系统与中断基础

第四部分 · 接口芯片与转换

怎么用这个合集
① 左侧点篇号切换,或按 ← / → 翻篇;
② 顶部搜索框可以搜「8259A」「补码」「中断向量表」这类关键词;
③ 每篇学完点右上角「标记已学完」,进度会存在浏览器里,关掉再打开还在;
④ 右上角 🌙 切深色模式;
⑤ 「打印全书」会隐藏侧栏、把 12 篇依次排版——建议按篇打印(先切到那一篇再 Ctrl+P)。

建议阅读顺序
第 1–2 篇是好懂的地基;第 3 篇(分段)是第一个坎,16 位怎么指 20 位地址想通了后面就顺; 第 4–6 篇讲指令和汇编,可以边看边在纸上模拟寄存器; 第 8 篇(I/O 与中断)是全书的转折点,后半段 9–12 篇全建立在这一篇上。
01 数制与编码
========================================================================
      微机原理与接口技术 · 第 1 篇
      数制、编码与运算基础 —— 30 分钟速通
========================================================================

写给谁:完全没接触过微机原理的人。不需要任何前置知识。
这一篇的目的:把"计算机只认 0 和 1"这件事讲透,为后面学 CPU 打地基。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

先问一个问题:计算机里面到底有什么?

一堆开关。每个开关只有两种状态:通电 / 断电。
用数字表示就是 1 / 0。

所以计算机的世界里【只有 0 和 1】—— 这不是设计者的偏好,
而是物理决定的:电压高一点叫 1,低一点叫 0,中间状态不可靠,不能用。

于是所有东西都得翻译成 0 和 1:
    数字 100      →  0110 0100
    负数 −5       →  1111 1011(这叫补码,第 3 站讲)
    字母 'A'      →  0100 0001
    汉字 '啊'     →  1011 0000 1010 0001

这一篇就解决一个问题:怎么把现实世界的东西翻译成 0/1,再翻译回来。

一共六站:

   第 1 站  位权:二进制到底怎么读                  (5 分钟)
   第 2 站  十六进制:程序员的速记法                (5 分钟)
   第 3 站  负数怎么表示:补码                      (7 分钟)
   第 4 站  溢出与符号扩展                          (5 分钟)
   第 5 站  BCD 码:十进制在计算机里的变通          (4 分钟)
   第 6 站  字符编码:ASCII 与汉字                  (4 分钟)

★★ 本篇的核心数字(后面每一篇都会用到)★★

    −5 的 8 位补码     = 1111 1011B = FBH
    8 位补码范围       = −128 ~ +127
    十六进制一位       = 4 位二进制(一个"半字节")
    BCD 的 25H + 37H   = 5CH,必须 DAA 调整成 62H
    'A' = 41H,'a' = 61H,'0' = 30H
    汉字「啊」机内码   = B0A1H


========================================================================
  第 1 站(0-5 分钟)  位权:二进制就是这么读的
------------------------------------------------------------------------

【先说结论】

    二进制和十进制读法完全一样,只有一点不同:进制不同。

    十进制:每位满 10 进 1,位权是 1、10、100、1000……
    二进制:每位满 2 进 1,位权是 1、2、4、8、16、32、64、128……

【怎么读一个二进制数】

    方法:把每一位的 0 或 1,乘以这一位的"位权",然后全部加起来。

    举例:0110 0100(8 位)

        位号    b7   b6   b5   b4   b3   b2   b1   b0
        位权   128   64   32   16    8    4    2    1
        值       0    1    1    0    0    1    0    0

        0×128 + 1×64 + 1×32 + 0×16 + 0×8 + 1×4 + 0×2 + 0×1
      = 64 + 32 + 4
      = 100

    所以 0110 0100B = 100(十进制)

    ★ 注意:只有写 1 的那几位才"出钱"。所以读二进制其实不用真的乘,
      只要把写着 1 的位权加起来就行。



【为什么位权是 2 的幂】

    因为满 2 进 1。第 0 位是最低位的 1,每往左一位就乘 2:

        b0 = 1
        b1 = 2
        b2 = 4
        b3 = 8
        b4 = 16
        b5 = 32
        b6 = 64
        b7 = 128

    这 8 个数字【背下来】,后面做题会飞快。

【几个常用术语(后面会一直用到)】

    位   bit     一个 0 或 1
    字节 byte    8 位,这是存储器的最小寻址单位
    字   word    16 位(对 8086 来说,一个字 = 2 字节)
    半字节 nibble 4 位,正好一位十六进制数

    ★ "字节"和"字"不要混:字节永远是 8 位,字的大小看 CPU
      (8086 是 16 位,现在的 CPU 是 64 位)。

【一个练习,一定要自己算一遍】

    "把 0000 1011 转成十进制"

        位权:128 64 32 16 8 4 2 1
        值  :0   0  0  0  1 0 1 1
        写 1 的位:8、2、1
        8 + 2 + 1 = 11

    所以 0000 1011B = 11 ✓


========================================================================
  第 2 站(5-10 分钟)  十六进制:程序员的速记法
------------------------------------------------------------------------

【为什么需要十六进制】

    二进制太长了。一个 16 位的数据要写 16 个 0/1,眼睛会看花:

        1000 0110 1010 0000     ← 这是多少?

    十六进制解决了这个问题:一位十六进制 = 4 位二进制。
    16 位的数只要写 4 位十六进制:

        1000 0110 1010 0000  =  8 6 A 0  =  86A0H

【为什么正好是 4 位】

    因为 2⁴ = 16。4 个二进制位正好有 16 种组合(0000 ~ 1111),
    和十六进制的 16 个数字(0~9、A~F)一一对应。

        0000 = 0      0100 = 4      1000 = 8      1100 = C
        0001 = 1      0101 = 5      1001 = 9      1101 = D
        0010 = 2      0110 = 6      1010 = A      1110 = E
        0011 = 3      0111 = 7      1011 = B      1111 = F

    这张对照表【必须背】,它是后面读机器码的基础。

【写法约定(很重要,不然考试扣分)】

        十六进制:64H、0A1H、0FFH    ← 末尾加 H;若首位是字母,前面加 0
        二进制  :01100100B          ← 末尾加 B
        十进制  :100、或者 100D
        八进制  :144O 或 144Q

    ★ 为什么 0A1H 要加个 0?因为汇编器会把 A1H 当成一个叫 A1H 的标号,
      加个 0 告诉它"这是个数字"。

【十进制 → 十六进制怎么转】

    方法:不断除以 16,记下余数,最后倒着读。

    例:把 100 转成十六进制

        100 ÷ 16 = 6 …… 余 4     (低位)
          6 ÷ 16 = 0 …… 余 6     (高位)

        倒着读:64

    所以 100 = 64H ✓(与第 1 站的 0110 0100B 一致)

【一个巨大的好处:十六进制直接看出二进制】

    因为一位十六进制就是 4 位二进制,所以:

        看到 86A0H,能立刻写出 1000 0110 1010 0000B
        看到 0110 0100B,能立刻写出 64H

    ★ 这就是为什么所有讲微机的书都用十六进制:
      它既能被人读,又能直接对应到二进制位。


========================================================================
  第 3 站(5-12 分钟)  负数怎么表示:补码
------------------------------------------------------------------------

    这是本篇最重要、也最容易懵的一站。请慢一点。

【问题:负号"+""−"是符号,不是 0/1】

    计算机里只有 0 和 1,没法存一个"负号"。
    那怎么表示 −5?

【第一种想法:拿最高位当符号位(这叫原码)】

        规定:最高位(b7)是符号位,0 表示正,1 表示负。

        +5 = 0 000 0101 = 05H
        −5 = 1 000 0101 = 85H

    看起来挺好。但有一个致命问题:减法算不了。

        5 + (−5) 应该是 0。用原码加一下:
          0000 0101
        + 1000 0101
        -----------
          1000 1010  = −10 ✗ 完全错了

    所以原码只能"看",不能"算"。

【第二种想法:反码】

        负数的反码 = 符号位不变,其余各位取反(0变1,1变0)。

        −5 的原码 = 1000 0101
        −5 的反码 = 1111 1010 = FAH

    加法试试:
          0000 0101  (+5)
        + 1111 1010  (−5 的反码)
        -----------
          1111 1111  = 反码的 0(应该是 0)

    结果 1111 1111 在反码里表示"负零"。差一点点,但还不是 0。

【第三种想法:补码(正确,全世界都用这个)】

        负数的补码 = 反码 + 1

        −5 的反码 = 1111 1010
        −5 的补码 = 1111 1011 = FBH

    加法试试:
          0000 0101  (+5)
        + 1111 1011  (−5 的补码)
        -----------
        1 0000 0000  = 最高位的 1 溢出丢掉,剩下 0000 0000 = 0 ✓

    成功了! 这就是补码的威力:加法和减法统一成加法。



【为什么补码能成功:模运算(钟表类比)】

    先看钟表:现在是 11 点,你想把它调到 10 点。

        方法一:倒回 1 小时(−1)
        方法二:前进 11 小时(+11)

    两个做法结果一样!因为在钟表上,"转一圈 12 小时"就回到原点,
    所以 −1 和 +11 是【同一件事】。这个"一圈"叫【模】,模 = 12。

    8 位二进制也是一个钟表:一共 256 个状态,转一圈 256。
    所以模 = 256,于是:

        −1  等价于  +255
        −5  等价于  +251

    验证 −5:251 的二进制 = 1111 1011 ✓ 正是 −5 的补码!



    ★ 一句话总结这一站:
      补码不是什么神秘规则,它只是"在模 256 的世界里,用正数代表负数"。
      取反加一,就是从 x 算出 256 − x 的快捷方式。

【求补码的实用口诀(考试就靠这个)】

    负数变补码:符号位不动,各位取反,末位加 1。

    例:−5(8 位)
        原码  1 000 0101
        取反  1 111 1010     ← 符号位不动,其余取反
        加 1  1 111 1011     ← 得 FBH ✓

    反过来,把补码变回真值:
        FBH = 1111 1011,最高位是 1 → 是负数
        取反加一:0000 0100 + 1 = 0000 0101 = 5
        加负号:−5 ✓

    ★ 注意:"取反加一"这个操作对补码是【自逆】的 ——
      做一次从真值到补码,再做一次就从补码回到真值。这是它最美的地方。

【补码的表示范围(必背)】

    8 位:−128 ~ +127
    16 位:−32768 ~ +32767

    规律:n 位补码 = −2^(n−1) ~ 2^(n−1) − 1

    为什么负数多一个?因为 0000 0000 表示 0,而 1000 0000 被用来
    表示 −128(它没有对应的正数)。所以负数比正数多一个。

    ★ 1000 0000B = 80H 是 −128,不是 −0。这是唯一的"不对称",
      考试常考。


========================================================================
  第 4 站(12-17 分钟)  溢出与符号扩展
------------------------------------------------------------------------

【溢出:算出来的结果装不下了】

    8 位补码只能表示 −128 ~ +127。
    如果两个数相加超过 127,就会"绕回去"变成负数。

    例:100 + 100

        100 = 0110 0100(正数)
        100 = 0110 0100(正数)
        和  = 1100 1000 = C8H

        C8H 的最高位是 1 → 按补码解释是负数:
        取反加一 = 0011 0111 + 1 = 0011 1000 = 56
        所以是 −56

    100 + 100 = −56 ✗ 明显错了,这就是【溢出】。
    这时 CPU 会把溢出标志 OF 置 1。

【怎么判断溢出(三句话)】

        正 + 正 得负   →  溢出
        负 + 负 得正   →  溢出
        符号不同相加   →  永不溢出

    为什么?因为一正一负相加,结果的绝对值一定比两者都小,
    不可能超出范围。

    ★ 特别注意:溢出和进位(CF)是两回事!
      进位是"最高位往外套了 1 位",溢出是"符号位错了"。
      无符号数看 CF,有符号数看 OF。这是考试的大坑。

【符号扩展:8 位怎么变成 16 位】

    有时候要把一个 8 位数放进 16 位寄存器。正数好办,前面补 0:

        +5(8位)  0000 0101
        +5(16位) 0000 0000 0000 0101   ← 前面补 8 个 0 ✓

    负数呢?不能补 0,否则变成很大的正数了:

        −5(8位)  1111 1011
        错误做法   0000 0000 1111 1011 = +251 ✗

    正确做法是【补符号位】(全部补 1):

        −5(16位) 1111 1111 1111 1011 = FFFBH = −5 ✓

    这就是指令 CBW(字节扩展成字)做的事:
    它看 b7,是 0 就补 0,是 1 就补 1。

    ★ 一句话:符号扩展 = 用【符号位】去填高位,不是用 0 去填。


========================================================================
  第 5 站(17-21 分钟)  BCD 码:十进制的变通
------------------------------------------------------------------------

【为什么需要 BCD】

    人用十进制,计算机用二进制。中间有个换算的麻烦。

    比如一个电子秤要显示"25",如果内部存的是二进制 19H(=25),
    要显示出来还得做一次除法(除以 10 取余),很麻烦。

    于是有个偷懒的办法:用 4 位二进制直接表示一位十进制数字。
    这叫 BCD 码(Binary-Coded Decimal,二进制编码的十进制)。

        十进制 25  →  BCD 码 0010 0101  = 25H

    注意:25H 在这里【不表示 37】!它表示十进制 25。
    这是 BCD 最反直觉的地方:同一个字节,按二进制读是 37,按 BCD 读是 25。

    ★ 所以看到一个 25H 的字面量,要先问:它是二进制数还是 BCD?
      这就是为什么汇编里 BCD 常量常写成 25H 并配注释。

【BCD 加法的坑:必须 DAA】

    现在算 25 + 37(BCD):

        0010 0101  (25)
      + 0011 0111  (37)
      -----------
        0101 1100  = 5CH

    按二进制算 25H + 37H = 5CH,没错。但 5CH 的低半字节是 C(=12),
    不是十进制数字!BCD 只允许 0~9。

    原因:低半字节 5 + 7 = 12,十进制应该"写 2 进 1",
    但硬件按十六进制算,进了 16 才进位。

    解决办法:算完加一条 DAA 指令,它会自动"掰"过来:

        低半字节 > 9 → 加 06H
        高半字节 > 9 → 加 60H

    本例:5CH + 06H = 62H ✓ 正确(25 + 37 = 62)



【什么时候用 BCD】

    · 需要直接显示十进制数字的场合(电子表、计算器、仪表)
    · 早期 CPU 没有浮点运算,用 BCD 做十进制小数
    · 现在的 CPU 里仍有 DAA/DAS/AAA/AAS 这些调整指令(8086 就有)

    ★ 现代编程基本不用 BCD 了,但它是考试的常客,
      因为它能同时考到"十六进制"和"进位"两个概念。


========================================================================
  第 6 站(21-25 分钟)  字符编码:ASCII 与汉字
------------------------------------------------------------------------

【ASCII:一个字节一个字符】

    ASCII 用 7 位表示一个字符(实际存成 8 位,最高位为 0)。
    一共 128 个字符:控制符 + 数字 + 字母 + 符号。

    必须记住的四个值:

        '0' = 30H = 48        'A' = 41H = 65
        ' ' = 20H = 32        'a' = 61H = 97

    三条实用规律:

        ① 数字 n 的 ASCII = 30H + n
           比如 '7' = 30H + 7 = 37H

        ② 大写字母连续:'A'=41H、'B'=42H……'Z'=5AH

        ③ 大小写只差 20H(第 5 位):
           小写 = 大写 OR 20H
           大写 = 小写 AND DFH

    ★ 第 ③ 条在汇编里超级有用:转换大小写只要一条 OR/AND 指令,
      不用查表。这也是为什么 ASCII 把大小写安排成只差一位。

【汉字编码:一个字节装不下】

    汉字有几万个,一个字节只有 256 种可能,装不下。
    所以汉字用【两个字节】。但两个字节直接存会有个问题:
    可能和 ASCII 冲突(比如某个汉字的字节正好是 41H,被当成 'A')。

    解决办法:让两个字节的最高位都是 1。这样取值范围就是 A1H~FEH,
    和 ASCII(00H~7FH)完全错开。

    三个名称要分清:

        区位码:把汉字按"区"和"位"编号,如「啊」= 16 区 01 位
        国标码:区位码 + 2020H
        机内码:国标码 + 8080H  ← 计算机内部真正存的

    以「啊」为例:

        区位码  16 区 01 位  →  十六进制 1001H
        国标码  1001H + 2020H = 3021H
        机内码  3021H + 8080H = B0A1H

    ★ 校验:「啊」在 GB2312 里的机内码确实就是 B0A1H ✓



【为什么要加两次 2020H 和 8080H】

    · 加 2020H:避开 ASCII 的控制字符区(00H~1FH)
    · 加 8080H:把两个字节的最高位都置 1,彻底避开 ASCII(00H~7FH)

    两字节最高位都是 1,所以计算机一看就知道"这是个汉字",
    不会误当成两个英文字母。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 二进制 0011 0101 的十进制值是多少?
  2. 十进制 200 的十六进制是多少?二进制呢?
  3. 为什么一位十六进制正好对应 4 位二进制?
  4. 写出 16 个十六进制数字与 4 位二进制的对照(A~F 四个即可)。
  5. −7 的 8 位原码、反码、补码分别是什么?
  6. 补码 1111 1100 的真值是多少?
  7. 8 位补码的表示范围?为什么负数比正数多一个?
  8. 100 + 50 用 8 位补码算,会不会溢出?为什么?
  9. 溢出(OF)和进位(CF)有什么区别?
 10. 怎么把一个 8 位负数扩展成 16 位?
 11. BCD 码 48H + 25H 的结果是多少?为什么要 DAA?
 12. 'F' 的 ASCII 是多少?把 'f' 改成 'F' 用什么操作?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 32 + 16 + 4 + 1 = 53。
  2. 200 = C8H = 1100 1000B。
  3. 因为 2⁴ = 16,4 位二进制恰好有 16 种组合,
     与十六进制的 16 个数字一一对应。
  4. A=1010、B=1011、C=1100、D=1101、E=1110、F=1111。
  5. −7 原码 = 1000 0111 = 87H;
     反码 = 1111 1000 = F8H;
     补码 = 1111 1001 = F9H。
  6. 1111 1100 最高位 1 → 负数;取反加一 = 0000 0011 + 1 = 0000 0100 = 4;
     所以真值是 −4。
  7. −128 ~ +127。因为 0000 0000 占了 0,剩下的负半边
     (1000 0000 ~ 1111 1111)能表示 128 个数,比正半边多一个;
     1000 0000 就是 −128。
  8. 100+50 = 150 > 127 → 溢出。正+正得负(150 的 8 位是 1001 0110,
     最高位 1,被解释成负数),OF = 1。
  9. 进位 CF 表示"最高位往外多出了一位"(无符号数不够装);
     溢出 OF 表示"结果的符号位错了"(有符号数不够装)。
     两者可能同时发生,也可能只发生一个。
 10. 符号扩展:高位全部填【符号位】。负数就全部填 1,
     例如 −5 从 FBH 扩成 FFFBH。
 11. 48H + 25H = 6DH。低半字节 8+5 = 13 > 9 → DAA 加 06H → 73H,
     即 73 ✓(48 + 25 = 73)。
 12. 'F' = 41H + 5 = 46H('A' 是 41H,往后数 5 个)。
     小写转大写:AND DFH(把第 5 位清 0)。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 把 25H 当成二进制 37,忘了它可能是 BCD 的 25
     BCD 和二进制长得一样,要靠上下文(或注释)区分。

  2. 负数"各位取反"时把符号位也取反了
     口诀第一步是"符号位不动"。符号位永远是 1。

  3. 溢出和进位混为一谈
     无符号看 CF,有符号看 OF。"正+正得负"才是溢出。

  4. 符号扩展时补 0
     负数要补 1。补 0 会把 −5 变成 +251。

  5. 认为 1000 0000 是 −0
     补码里没有 −0,它就是 −128。

  6. BCD 加完忘了 DAA
     只要结果是"要给人看的十进制",就必须调整。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 2 篇(8086 结构):寄存器宽度(8/16 位)、标志位(CF/OF/SF/ZF)
      全部建立在这一篇之上。
    · 第 3 篇(分段与寻址):物理地址 = 段 × 16 + 偏移,
      那个"×16"就是左移 4 位,是十六进制的直接应用。
    · 第 4~5 篇(指令系统):机器码全是十六进制,寻址方式靠编码位区分。
    · 第 6 篇(汇编程序):数据段里 DB/DW 定义的就是这些编码。
    · 第 9 篇(中断):中断类型号、中断向量表的地址计算全是十六进制。

    一句话:这一篇不学后面寸步难行,但它本身只有两个重点 ——
    十六进制怎么读,和补码为什么能算。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    图已设置避免跨页断开,正文按 10pt 排版。
02 8086结构与寄存器
========================================================================
      微机原理与接口技术 · 第 2 篇
      8086 微处理器:结构与寄存器 —— 30 分钟速通
========================================================================

前置:第 1 篇(数制与补码)。如果你还不会读十六进制,先回去看一眼。
这一篇的目的:认识 8086 这个"人"—— 它有几个部门、几个抽屉、怎么干活。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

8086 是 1978 年的芯片,现在看很原始,但它是整个 x86 家族的祖宗。
你现在用的电脑,开机那一瞬间仍然是按 8086 的方式启动的。

这一篇只回答三个问题:

    ① 8086 里面有什么?        → BIU + EU 两个部分
    ② 它用什么存东西?          → 14 个寄存器
    ③ 它怎么知道自己算什么?    → FLAGS 标志位

一共六站:

   第 1 站  8086 的规格和外部引脚            (5 分钟)
   第 2 站  BIU 与 EU:为什么要分成两半      (6 分钟)
   第 3 站  通用寄存器和指针变址寄存器        (6 分钟)
   第 4 站  段寄存器与控制寄存器              (5 分钟)
   第 5 站  FLAGS:9 个标志位                (5 分钟)
   第 6 站  一条指令是怎么走完的              (3 分钟)

★★ 本篇的核心数字 ★★

    数据总线 16 位        一次搬 2 字节
    地址总线 20 位        2^20 = 1MB 寻址空间
    寄存器 16 位          只能直接寻址 64KB ← 矛盾就在这
    AX = 1234H 时         AH=12H(=18)、AL=34H(=52)
    7FH + 01H 后          AF=1、SF=1、OF=1,FLAGS 低 12 位 = 0890H
    FLAGS 中 CF 在 bit0,OF 在 bit11


========================================================================
  第 1 站(0-5 分钟)  8086 的规格和外部引脚
------------------------------------------------------------------------

【先看几个关键数字】

    数据总线:16 位   → 一次能搬 2 个字节
    地址总线:20 位   → 能寻址 2^20 = 1MB
    内部寄存器:16 位 → 只能直接表示 64KB 的地址

    ★ 第三行和第二行是矛盾的:寄存器只有 16 位,怎么指 20 位的地址?
      这就是【分段】要解决的问题,第 3 篇专门讲。
      现在你只要记住这个矛盾存在。

    时钟:8086 标准 5MHz(8088 是 4.77MHz,就是当年 IBM PC 用的)
    封装:40 脚双列直插(DIP-40)

【几个必须知道的引脚(了解即可,考试可能考)】

    AD0 ~ AD15   16 根,地址和数据【分时复用】同一组引脚
                 ★ 为什么要复用?因为 40 个引脚不够用。
                   同一根线,第一个时钟周期送地址,后面的周期传数据。
                   ALE 信号就是告诉外部:"现在线上是地址,快锁存!"

    A16/S3 ~ A19/S6   高 4 位地址,也是复用的

    ALE          地址锁存允许,高电平表示"总线上现在是地址"
    M/IO         高电平访存,低电平访端口
    RD / WR      读 / 写选通
    READY        外设"我准备好了"的应答(不快就拉低,让 CPU 等)
    INTR / NMI   可屏蔽 / 不可屏蔽中断请求(第 8 篇)
    RESET        复位,复位后 CS=FFFFH、IP=0000H
    CLK          时钟输入

【复位后从哪开始执行】

    复位时:CS = FFFFH,IP = 0000H

    物理地址 = FFFF0H + 0000H = FFFF0H

    所以 8086 一上电就从【FFFF0H】开始执行 ——
    这个位置在 1MB 空间的最高端。主板厂商在那里放一条跳转指令,
    跳到 BIOS 的真正入口。

    ★ 这是个很聪明的设计:无论硬件怎么变,
      上电第一条指令的地址永远是固定的。


========================================================================
  第 2 站(5-11 分钟)  BIU 与 EU:为什么要分成两半
------------------------------------------------------------------------

    这是 8086 最核心的设计思想,理解了它,后面的流水线、
    指令队列、总线周期全都能串起来。

【两个部门】

    把 8086 想成一个公司,有两个部门:

        BIU(总线接口单元)—— 采购部
            负责和外界打交道:取指令、读数据、写数据
            内部有:段寄存器、IP、地址加法器、指令队列、总线控制

        EU(执行单元)—— 生产部
            负责运算:加减乘除、逻辑运算、判断
            内部有:通用寄存器、ALU、标志寄存器

【关键:两个部门各干各的】

    EU 算一条指令要用好几个时钟周期,这期间它用不着总线。
    如果让 EU 自己去取指令,那总线就有一半时间是闲着的。

    于是 8086 让 BIU【提前把后面的指令取回来,放进队列】。
    这个队列叫【指令队列】,6 个字节,能放几条指令。

    这样:BIU 在取第 3 条指令的同时,EU 正在算第 1 条。
    两者同时进行 —— 这就是【流水线】。



【指令队列的两个状态】

    队列满了(6 字节装满)→ BIU 停止取指,等着
    队列空了           → EU 停下来等,BIU 赶紧去取

    所以流水线的效率取决于"取"和"算"谁更快。如果取指比执行慢,
    EU 就经常要等(叫"流水线断流"),这时流水线的好处就打折扣了。

【这就是为什么 8086 比 8080 快很多】

    8080 是"取一条、算一条、再取下一条",全程串行。
    8086 有指令队列,取和算重叠,同样主频下明显更快。

【★ 一个重要的推论(考试常考)】

    因为 EU 和 BIU 是分开的,所以【修改段寄存器和 IP 的指令】
    会清空指令队列!

    为什么?因为队列里的指令是按旧地址取的,
    CS 或 IP 一改,那些指令就"取错地方"了,必须扔掉重取。

    这就是为什么 JMP、CALL、RET、INT 这些指令执行时,
    会看到"指令队列被清空"的现象,也是它们较慢的原因。


========================================================================
  第 3 站(11-17 分钟)  通用寄存器和指针变址寄存器
------------------------------------------------------------------------

    8086 一共 14 个寄存器,别怕,分组以后就不多了:

        通用寄存器   4 个:AX BX CX DX
        指针变址     4 个:SP BP SI DI
        段寄存器     4 个:CS DS SS ES
        控制寄存器   2 个:IP FLAGS



【通用寄存器:AX BX CX DX】

    四个都是 16 位,都能存数据。但它们各自有个"专长":

        AX(累加器 Accumulator)
            · 乘除法必须用它(MUL/DIV 的结果或操作数在里面)
            · 所有 I/O 指令(IN/OUT)必须经过它
            · 字符串操作也用
            ★ 最忙的一个

        BX(基址 Base)
            · 唯一能用来"间接寻址"的通用寄存器
              例:MOV AX, [BX]  ← 把 BX 指的地址里的数取出来
            · 可以做查表(XLAT 指令用它)

        CX(计数 Count)
            · 循环指令 LOOP 用它做计数器
            · 串操作(MOVSB 等)用它表示重复次数
            ★ 名字就说明了用途:Count

        DX(数据 Data)
            · 乘除法时存放高 16 位(AX 放低 16 位)
            · I/O 端口地址超过 255 时,端口号放在 DX 里

    ★ 记忆法:AX 是"干活的",BX 是"指路的",CX 是"数数的",DX 是"帮忙的"。

【它们都能拆成两半用】

    这是 8086 的"向后兼容"设计:它要能处理 8086 那个时代的 8 位数据。

        AX = AH(高字节)+ AL(低字节)
        BX = BH + BL
        CX = CH + CL
        DX = DH + DL



    例:AX = 1234H
        AH = 12H = 18(十进制)
        AL = 34H = 52(十进制)
        校验:18 × 256 + 52 = 4660 = 1234H ✓

    ★ 注意:SP BP SI DI 这 4 个【不能拆】,它们只能按 16 位用。
      因为它们要装地址,8 位地址没意义。

【指针和变址寄存器:SP BP SI DI】

    这 4 个专门用来"装地址",各有各的场合:

        SP(堆栈指针 Stack Pointer)
            · 永远指向栈顶
            · PUSH 时 SP 减 2,POP 时 SP 加 2(注意:栈是向下长的!)
            · 不能拿来算数(虽然语法上能用,但会破坏堆栈)

        BP(基址指针 Base Pointer)
            · 用来访问堆栈里的"局部变量"
            · 默认搭配 SS 段(SP 也是)
            · 这是它和 BX 最大的区别

        SI(源变址 Source Index)
        DI(目的变址 Destination Index)
            · 串操作用:从 SI 指向的地方搬到 DI 指向的地方
            · 默认搭配 DS 段

【★ 默认段搭配(这是第 4 篇寻址方式的基础,先记住)】

        用 BX、SI、DI 寻址  → 默认在 DS 段
        用 BP、SP 寻址       → 默认在 SS 段(堆栈段)

    为什么?因为 BX/SI/DI 是访问"数据"的,数据在数据段;
    BP/SP 是访问"堆栈"的,堆栈在堆栈段。设计得很合理。

    这个规律解释了一件事:为什么 BP 不能像 BX 那样随便用?
    因为它默认指向堆栈段,不在堆栈里用它会指错地方。


========================================================================
  第 4 站(17-22 分钟)  段寄存器与控制寄存器
------------------------------------------------------------------------

【段寄存器:CS DS SS ES】

    四个 16 位寄存器,每个存一个【段基址】:

        CS(Code Segment)    代码段 —— 指令放在哪
        DS(Data Segment)    数据段 —— 变量放在哪
        SS(Stack Segment)   堆栈段 —— 栈放在哪
        ES(Extra Segment)   附加段 —— 额外的地方(串操作用)

    ★ 这四个一起来解决"16 位寄存器怎么指 20 位地址"的问题:

        物理地址 = 段寄存器 × 16 + 偏移

        "× 16"就是左移 4 位,也就是十六进制数末尾补一个 0。

    例:CS = 1234H,IP = 5678H
        物理地址 = 12340H + 5678H = 179B8H

    ★ 注意:段基址必须是 16 的倍数(因为末尾那个 0 是硬加上去的),
      所以一个段最多 64KB,段与段之间可以重叠。

【控制寄存器:IP 和 FLAGS】

    IP(指令指针 Instruction Pointer)
        · 永远指向【下一条】要执行的指令
        · 注意是"下一条",不是"当前"。因为取指时就自动加了
        · 不能用 MOV 直接改!只能用 JMP/CALL/RET/INT 间接改
        · 和 CS 一起决定下一条指令的地址

    FLAGS(标志寄存器)
        · 16 位,但只用了 9 位
        · 存的是"上一条指令结果的特征"
        · 下一站专门讲

【为什么 IP 不能直接 MOV 改】

    因为程序的执行流是靠 IP 控制的。如果允许随便改 IP,
    就等于允许"跳到任意地方",那程序就没法保证正确性了。

    所以 8086 规定:只能通过跳转、调用、返回、中断这些
    【有明确语义】的指令来改 IP。这是个安全设计。

    ★ 类比:IP 像"当前阅读位置"的书签。你可以用"翻到第 5 页"
      (JMP)这种正规操作改它,但不该直接把书签撕下来贴到别处。


========================================================================
  第 5 站(22-27 分钟)  FLAGS:9 个标志位
------------------------------------------------------------------------

    这是本篇最容易记混的地方,但也是最常考的。

【标志位的位置(记住这张表)】

        bit 0   CF   进位标志
        bit 2   PF   奇偶标志
        bit 4   AF   辅助进位标志
        bit 6   ZF   零标志
        bit 7   SF   符号标志
        bit 8   TF   陷阱标志(单步调试用)
        bit 9   IF   中断允许标志
        bit 10  DF   方向标志(串操作用)
        bit 11  OF   溢出标志

    其余位(1、3、5、12~15)不用,固定值。

    记忆口诀:CF 在最右边(bit0),OF 在 bit11,中间按顺序排,空位跳过。

    对应的十六进制值(写程序时常用):

        CF = 0001H    PF = 0004H    AF = 0010H
        ZF = 0040H    SF = 0080H    TF = 0100H
        IF = 0200H    DF = 0400H    OF = 0800H



【逐个讲清楚(重点讲 CF 和 OF 的区别)】

    CF(Carry Flag)进位标志
        只看【无符号数】:最高位有没有往外套一位
        例:FFH + 01H = 100H,套出了 1 位 → CF = 1

    OF(Overflow Flag)溢出标志
        只看【有符号数】:符号位有没有被算错
        判断法:正+正得负,或 负+负得正 → OF = 1

    ★ 这两个最容易混。一句话区分:
      CF 关心"装不下"(无符号角度),OF 关心"符号错了"(有符号角度)。

    ZF(Zero Flag)零标志
        结果为 0 时置 1。所有的"相等判断"都靠它:
        CMP A, B 之后若 ZF=1,说明 A = B

    SF(Sign Flag)符号标志
        结果的最高位(bit7 或 bit15)。SF=1 表示"结果是负数"

    PF(Parity Flag)奇偶标志
        结果最低字节中 1 的个数为【偶数】时置 1
        ★ 注意是"偶数置 1",容易记反

    AF(Auxiliary Carry Flag)辅助进位
        低半字节(bit3 → bit4)有没有进位。串操作和 DAA 用

    TF(Trap Flag)陷阱标志
        置 1 则每执行一条指令就产生一次单步中断 —— 调试器的原理

    IF(Interrupt Flag)中断允许
        置 1 才允许响应可屏蔽中断。CLI 清 0(关中断)、STI 置 1(开中断)

    DF(Direction Flag)方向标志
        串操作时地址是增还是减。CLD 清 0(增)、STD 置 1(减)

【实际算一遍:7FH + 01H】

    7FH = 0111 1111(最大的 8 位正数,等于 127)
    01H = 0000 0001

    相加 = 1000 0000 = 80H

    逐位看:
        SF = 1    最高位变成 1 了,按补码这是负数
        OF = 1    127 + 1 = 128 > 127,溢出!正+正得负 → OF=1
        AF = 1    低半字节 F + 1 = 10H,产生了半字节进位
        CF = 0    没有超出 8 位(1000 0000 还在 8 位里)
        ZF = 0    结果不是 0
        PF = 0    80H = 1000 0000,只有 1 个 1,是奇数 → PF=0

    所以置 1 的是 AF、SF、OF,FLAGS 低 12 位 = 0890H。

    ★ 这个例子特别好:它同时展示了 SF 和 OF 的区别 ——
      结果"看起来是负的"(SF=1),而且确实是溢出(OF=1),
      但两者是分别判断的两件事。


========================================================================
  第 6 站(27-30 分钟)  一条指令是怎么走完的
------------------------------------------------------------------------

【三个阶段】

    ① 取指(BIU 干)
       CS:IP 算出物理地址 → 从存储器读出一条指令 → 放进指令队列
       → IP 自动加上这条指令的长度(指向下一条)

    ② 译码(EU 干)
       看操作码,判断这是条什么指令、操作数在哪

    ③ 执行(EU 干)
       取操作数、运算、写回结果、更新 FLAGS



【几个时间概念(了解)】

    时钟周期   T    最小的单位,5MHz 时 T = 200ns
    总线周期        一次总线操作(读或写)用的时间,通常 4 个 T
    指令周期        执行一条指令用的时间,由若干个总线周期组成

    ★ 8086 访问一次存储器要 4 个时钟周期。5MHz 下就是 800ns。
      所以它一秒钟最多做 125 万次访存 —— 这就是当年的"快"。

【为什么要知道这些】

    因为后面讲接口时,"CPU 等外设"(READY 信号)、
    "中断响应时序"、"DMA 抢总线"这些事情,全都要用这些时间概念。
    现在有个印象就够了。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 8086 的数据总线和地址总线各多少位?寻址空间多大?
  2. BIU 和 EU 各负责什么?为什么要分开?
  3. 指令队列有几个字节?队列满了/空了分别会怎样?
  4. 四个通用寄存器各自有什么"专长"?
  5. AX = 5678H 时,AH 和 AL 分别是多少(十六进制和十进制)?
  6. 哪几个寄存器不能拆成 8 位用?为什么?
  7. 用 BX 寻址默认在哪个段?用 BP 呢?
  8. 段寄存器有哪几个?各自的用途?
  9. 物理地址的计算公式?CS=2000H、IP=0100H 时物理地址是多少?
 10. IP 为什么不能用 MOV 直接改?
 11. CF 和 OF 有什么区别?FFH+01H 时它们分别是多少?
 12. 7FH + 01H 之后哪些标志位置 1?为什么 SF 和 OF 同时为 1?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 数据总线 16 位,地址总线 20 位,寻址 2^20 = 1MB。
  2. BIU 负责取指令和读写数据(对外),EU 负责运算(对内)。
     分开是为了让"取指令"和"执行指令"能同时进行(流水线)。
  3. 6 字节。满了 BIU 停止取指等待;空了 EU 停止等待 BIU 去取。
  4. AX:乘除法、I/O 必须用;BX:可间接寻址、查表;
     CX:循环计数、串操作次数;DX:乘除高 16 位、大端口号。
  5. AH = 56H = 86;AL = 78H = 120。校验:86×256+120 = 22136 = 5678H。
  6. SP、BP、SI、DI。因为它们装的是地址,8 位地址没有意义。
  7. BX 默认 DS 段;BP 默认 SS 段。
  8. CS 代码段、DS 数据段、SS 堆栈段、ES 附加段。
  9. 物理地址 = 段寄存器×16 + 偏移。2000H×16 = 20000H,
     加 0100H = 20100H。
 10. 因为 IP 控制执行流程,随意修改会导致程序跳到错误位置。
     只能用 JMP/CALL/RET/INT 等有明确语义的指令间接修改。
 11. CF 看无符号(有没有往外套一位),OF 看有符号(符号位有没有算错)。
     FFH+01H:结果 00H,套出了 1 位 → CF=1;但 FFH=−1、01H=1,
     −1+1=0 没有溢出 → OF=0。
 12. AF、SF、OF 置 1。SF=1 因为结果最高位变 1;
     OF=1 因为 127+1 超出 8 位有符号范围(正+正得负);
     AF=1 因为低半字节 F+1 产生了进位。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 把 CF 和 OF 混为一谈
     无符号看 CF,有符号看 OF。"正+正得负"才是 OF=1。

  2. 以为 PF 是"奇数置 1"
     恰恰相反:结果低字节里 1 的个数为【偶数】时 PF=1。

  3. 以为 IP 可以用 MOV 改
     不行。只能 JMP/CALL/RET/INT。

  4. 认为"寄存器 16 位 = 只能寻址 64KB"
     那是【不用分段】时。用了段地址以后能寻址 1MB,第 3 篇讲。

  5. 忘了栈是向下长的
     PUSH 时 SP【减】2,POP 时 SP【加】2。很多人会记反。

  6. 以为 AX 拆成 AH/AL 时 AH 是低位
     AH 是【高】字节(A-High),AL 是【低】字节(A-Low)。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 3 篇(存储器与分段):段寄存器 ×16 + 偏移 = 20 位物理地址,
      这一篇埋的"矛盾"在那里解决。
    · 第 4 篇(寻址方式):默认段搭配(BX→DS、BP→SS)直接用。
    · 第 5 篇(指令系统):每条指令都要更新 FLAGS,条件转移靠它。
    · 第 9 篇(8259A 中断):IF 标志位控制中断的开关。
    · 第 10 篇(8255A):端口地址与 M/IO 引脚配合。

    一句话:这一篇是"认识零件",后面每一篇都是"用零件搭东西"。
    重点记三样:两个部门(BIU/EU)、14 个寄存器、9 个标志位。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    结构图与位图已设置避免跨页断开,正文按 10pt 排版。
03 存储器组织与分段
========================================================================
      微机原理与接口技术 · 第 3 篇
      存储器组织与分段 —— 30 分钟速通
========================================================================

前置:第 1 篇(十六进制)、第 2 篇(寄存器、CS/DS/SS/ES、IP)。
这一篇解决第 2 篇留下的那个矛盾:16 位寄存器怎么指 20 位地址。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

先回顾那个矛盾:

    8086 的寄存器只有 16 位  →  最大能表示 65535 = 64KB
    8086 的地址总线有 20 位  →  能寻址 1MB

    差了 4 位。怎么办?

Intel 的答案非常聪明:再加一个寄存器当"起点"。

        物理地址 = 段寄存器 × 16 + 偏移

    段寄存器 16 位,×16 以后变成 20 位;偏移 16 位,加起来还是 20 位。
    两个 16 位的数凑出一个 20 位的地址 —— 这就是【分段】。

一共六站:

   第 1 站  存储器怎么编号:字节编址                (4 分钟)
   第 2 站  小端模式:一个字怎么放进两个抽屉        (4 分钟)
   第 3 站  ★ 16 位怎么指 20 位:分段               (7 分钟)
   第 4 站  段重叠与逻辑地址/物理地址               (5 分钟)
   第 5 站  1MB 空间分布与堆栈段                    (6 分钟)
   第 6 站  综合练习                                (4 分钟)

★★ 本篇的核心数字 ★★

    1MB = 2^20 = 1,048,576 字节
    物理地址 = 段 × 16 + 偏移
    1234H:5678H → 12340H + 5678H = 179B8H
    同一地址 179B8H 有 5 种写法(段可重叠)
    段基址 65536 个,每个段最大 64KB
    FFFF:FFFF → 10FFEFH → 截断成 0FFEFH(A20 的由来)
    中断向量表 = 256 × 4 = 1KB,位于 00000H ~ 003FFH


========================================================================
  第 1 站(0-4 分钟)  存储器怎么编号
------------------------------------------------------------------------

【一个抽屉一个字节】

    把存储器想成一排抽屉,每个抽屉装 1 个字节(8 位),
    每个抽屉有个编号,叫【地址】。

        地址 00000H 的抽屉里装着 1 个字节
        地址 00001H 的抽屉里装着 1 个字节
        ……
        地址 FFFFFH 的抽屉(1MB 的最后一个)

    ★ 关键:地址是按字节编的(byte-addressable)。
      哪怕你只想改 1 个位,也得读写整个字节。

【地址位数决定容量】

        20 根地址线 → 2^20 = 1,048,576 个地址 → 1MB

    这个关系要会算:

        10 根地址线 → 2^10 = 1024 = 1KB
        20 根地址线 → 1MB
        32 根地址线 → 4GB

    ★ 注意"KB"里的 K 是 1024 不是 1000。所以 1MB = 1024KB = 1048576 字节。

【容量换算表(背下来)】

        1B   = 8 bit
        1KB  = 1024 B     = 2^10
        1MB  = 1024 KB    = 2^20
        1GB  = 1024 MB    = 2^30

    常用结论:地址线每多 1 根,容量翻一倍。


========================================================================
  第 2 站(4-8 分钟)  小端模式:一个字怎么放
------------------------------------------------------------------------

【问题】

    8086 一次能处理 16 位(一个字 = 2 字节)。
    但存储器是按字节编址的(一个地址一个字节)。
    所以一个字要占【两个相邻地址】。

    问:高位字节放前面还是后面?

【x86 的答案:低字节放低地址(小端)】

        把字 1234H 存到 1000H:

            1000H  ←  34H     (低字节放低地址)
            1001H  ←  12H     (高字节放高地址)

    这叫【小端模式】(little-endian,小的一端在前)。



    为什么会这样设计?因为 CPU 算加法是【从低位算起】的。
    低位先算完,就可以先存下去,不用等高位算完 —— 省时间、省硬件。

    ★ 反过来叫【大端模式】(big-endian),高字节放低地址。
      网络协议(TCP/IP)用大端,所以做网络编程时要转换字节序。

【自己验一遍】

    从 1000H 读一个【字】:CPU 先读 1000H 得 34H(低字节),
    再读 1001H 得 12H(高字节),拼成 1234H。

    验证:0x1234 的低字节就是 0x34,高字节是 0x12 ✓

【规则字与非规则字】

    如果一个字的起始地址是【偶数】(比如 1000H),叫【规则字】。
    如果起始地址是【奇数】(比如 1001H),叫【非规则字】。

    ★ 8086 读规则字只要 1 个总线周期,读非规则字要 2 个。
      所以编译器会把变量对齐到偶地址 —— 这是"内存对齐"的起源。
      你在 C 语言里学的 struct 对齐,根源就在这里。


========================================================================
  第 3 站(8-15 分钟)  ★ 16 位怎么指 20 位:分段
------------------------------------------------------------------------

    这是本篇、也是整门课最重要的一站。

【回忆矛盾】

    寄存器 16 位 → 只能表示 0 ~ 65535 → 64KB
    地址总线 20 位 → 能寻址 1MB

    只用一个寄存器,永远指不到 1MB 的后半部分。

【Intel 的办法:拿两个数凑】

        物理地址 = 段基址 × 16 + 偏移

    · 段基址放在段寄存器里(CS/DS/SS/ES),16 位
    · 偏移放在 IP 或通用寄存器里,16 位
    · 段基址 × 16 会变成 20 位(因为左移 4 位)
    · 再加偏移,结果还是 20 位

【为什么是 ×16 而不是 ×8 或 ×32】

    因为要"凑够 20 位"。16 位左移 4 位正好补到 20 位。
    而左移 4 位在十六进制里就是【末尾补一个 0】—— 极其好算。

        1234H × 16 = 12340H     ← 看,就是加了个 0

    ★ 这个设计还有个副产品:段基址必须是 16 的倍数,
      也就是说 1MB 空间被切成 65536 个"16 字节"的小格子,
      段基址只能落在格子边界上。



【算一遍(一定要会)】

    已知 CS = 1234H、IP = 5678H,求下一条指令的物理地址。

        段基址 × 16:  1234H × 16 = 12340H
        加偏移:       12340H + 5678H = 179B8H

        物理地址 = 179B8H ✓

    验算(用十进制核对):
        1234H = 4660,×16 = 74560
        5678H = 22136
        74560 + 22136 = 96696
        96696 的十六进制 = 179B8H ✓

【两个术语要分清】

    逻辑地址(也叫虚拟地址):段:偏移 的形式,如 1234H:5678H
    物理地址:CPU 真正送到地址总线上的 20 位数,如 179B8H

    ★ 程序员写程序时用逻辑地址(段:偏移),
      CPU 内部用地址加法器自动算成物理地址。
      这个过程叫【地址转换】,是第 7 篇"分页"要接着讲的话题。

【段寄存器怎么分工】

        CS:IP   →  取下一条指令        (代码段)
        DS:BX   →  取数据              (数据段)
        SS:SP   →  取堆栈数据          (堆栈段)
        ES:DI   →  串操作的目的        (附加段)

    每个段寄存器管一类用途,这是"逻辑分段",
    而物理上段与段可以任意重叠(下一站讲)。


========================================================================
  第 4 站(15-20 分钟)  段重叠与逻辑地址/物理地址
------------------------------------------------------------------------

【段可以重叠】

    同一个物理地址 179B8H,可以写成:

        1234H:5678H      (12340H + 5678H)
        1200H:59B8H      (12000H + 59B8H)
        179BH:0008H      (179B0H + 0008H)← 偏移只有 8
        1790H:00B8H      (17900H + 00B8H)
        1000H:79B8H      (10000H + 79B8H)

    五种写法,指向同一个抽屉。



    ★ 这说明 8086 的"段"不是一道隔离墙,只是一个"起点"。
      段与段之间只差 16 字节就能再开一个段,所以任意两个段都可能重叠。

    这和现代 CPU 的"分页"完全不一样 —— 分页是硬隔离的。
    这一点考试常考:"8086 的段可以重叠吗?" 答案是可以。

【段的个数】

    段基址是 16 位 → 0000H ~ FFFFH,共 65536 个可能值
    每个段最大 64KB(偏移 16 位)
    1MB / 16 = 65536 个对齐位置

    ★ 所以:段很多、很小(最大 64KB)、还能重叠。

【段寄存器的值怎么变】

    · CS、DS、SS、ES 都可以用 MOV 改(比如 MOV AX, DATA 然后 MOV DS, AX)
    · 但 CS 不能直接用 MOV 改(只能用 JMP FAR / CALL FAR / RETF / INT 改)
    · 原因和第 2 篇讲 IP 一样:CS:IP 决定执行流,乱改会出事


========================================================================
  第 5 站(20-26 分钟)  1MB 空间分布与堆栈段
------------------------------------------------------------------------

【1MB 不是随便用的】

    低端和高端都有固定用途,这是 PC 的"约定"。



    必须记住的三块:

        00000H ~ 003FFH   中断向量表,1KB
                          (256 个中断 × 4 字节)
        B8000H ~ BBFFFH   彩色字符显存,32KB
                          (往这里写字符,屏幕上就出现 —— 第 10 篇会用到)
        FFFF0H            复位后的第一条指令

【中断向量表为什么正好 1KB】

    8086 支持 256 种中断(类型号 00H ~ FFH)。
    每种中断需要一个入口地址,占 4 字节(2 字节 IP + 2 字节 CS)。

        256 × 4 = 1024 字节 = 1KB ✓

    第 n 号中断的入口地址存放在:n × 4 处

        中断 00H → 0000H
        中断 08H → 0020H      (8×4 = 32 = 20H)
        中断 10H → 0040H
        中断 1CH → 0070H

    ★ 这个表放在最低端,是为了"快":中断发生时 CPU 直接用
      "类型号 × 4" 算出地址,不用查任何表。

【堆栈段:向下生长的抽屉】

    堆栈是"后进先出"(LIFO)的存储区,用 SS:SP 定位。

    关键:栈是向下长的。

        PUSH 时:SP 先减 2,再把数据存进去
        POP  时:先从 SP 处取数据,再 SP 加 2



    例:初始 SP = 0100H

        PUSH AX  →  SP = 00FEH,AX 存到 SS:00FEH
        PUSH BX  →  SP = 00FCH,BX 存到 SS:00FCH
        POP  CX  →  从 SS:00FCH 取出,SP 变回 00FEH

    ★ 为什么"先减后存"?因为 SP 要永远指向"最后一个压入的数据"。
      如果先存后减,SP 就指向空位置了,POP 会取错。

    ★ 为什么向下长?这样堆栈段可以从段的最高地址往下用,
      不会和代码段、数据段冲突。

【堆栈溢出】

    如果 PUSH 太多,SP 会一直减到 0000H 然后绕回 FFFFH,
    覆盖掉堆栈段里的其他内容 —— 这叫【堆栈溢出】。
    这就是程序"栈溢出"崩溃的原始形态。


========================================================================
  第 6 站(26-30 分钟)  综合练习
------------------------------------------------------------------------

【练习一:算物理地址】

    DS = 2000H,BX = 0300H,求 [BX] 指向的物理地址。

        2000H × 16 = 20000H
        20000H + 0300H = 20300H

        答案:20300H

【练习二:给物理地址,反推段和偏移】

    物理地址 = 45678H,段基址取 4000H,偏移是多少?

        45678H − 40000H = 5678H

        答案:偏移 5678H,逻辑地址 4000H:5678H

【练习三:判断能不能表示】

    CS = FFFFH、IP = FFFFH,物理地址是多少?有没有问题?

        FFFFH × 16 = FFFF0H
        FFFF0H + FFFFH = 10FFEFH   ← 需要 21 位!

        但 8086 只有 20 根地址线,所以第 21 位(那个 1)被丢掉了:
        实际访问 0FFEFH

    ★ 这就是著名的【A20 问题】的由来。
      1MB 以上的地址会"绕回"到低端。
      后来 80286 有 24 根地址线,为了兼容老程序,
      主板专门加了 A20 门电路来控制第 21 根线(A20)通不通。
      HIMEM.SYS 里的 "A20 控制"就是干这个的。

【练习四:栈指针】

    初始 SP = FFF0H,连续 PUSH 3 次后 SP = ?

        FFF0H − 2 = FFEEH
        FFEEH − 2 = FFECH
        FFECH − 2 = FFEAH

        答案:FFEAH(每次减 2,因为一次压入一个字)


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 地址是按什么单位编的?20 根地址线能寻址多少?
  2. 1KB = 多少字节?1MB = 多少字节?
  3. 小端模式是什么意思?字 5678H 存到 2000H 时,2000H 和 2001H 各是什么?
  4. 什么是规则字?规则字有什么好处?
  5. 物理地址的计算公式?
  6. 为什么段基址要 ×16,不是 ×8 或 ×32?
  7. DS=3000H、BX=1234H,物理地址是多少?
  8. 物理地址 179B8H 可以写成哪几种段:偏移?(至少写三种)
  9. 8086 的段可以重叠吗?这和现代 CPU 的分页有什么不同?
 10. 中断向量表在哪个地址范围?为什么正好 1KB?第 08H 号中断的入口存在哪?
 11. PUSH 时 SP 是加还是减?为什么?
 12. CS=FFFFH、IP=FFFFH 时物理地址是多少?要注意什么现象?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 按字节编址。2^20 = 1MB。
  2. 1KB = 1024 字节;1MB = 1048576 字节。
  3. 低字节放低地址,高字节放高地址。2000H 存 78H,2001H 存 56H。
  4. 起始地址为偶数的字。好处是只需一个总线周期就能读完
     (一次读两个字节)。
  5. 物理地址 = 段寄存器 × 16 + 偏移。
  6. 因为要凑够 20 位:16 位左移 4 位正好变成 20 位。
     而且左移 4 位在十六进制里就是末尾补 0,算起来最方便。
  7. 30000H + 1234H = 31234H。
  8. 1234H:5678H、1200H:59B8H、179BH:0008H(任写三个即可)。
  9. 可以重叠。8086 的段只是一个「起点」,不是隔离墙;
     现代 CPU 的分页是硬隔离的,一个页不能随意跨到另一个页。
 10. 00000H ~ 003FFH。因为 256 种中断 × 4 字节 = 1024 字节 = 1KB。
     第 08H 号中断的入口存在 08H×4 = 20H,即 0020H ~ 0023H。
 11. 减。因为 SP 要永远指向最后一个压入的数据(栈顶),
     先减 2 再存,SP 就在新数据的地址上。
 12. 10FFEFH。但 8086 只有 20 位地址线,第 21 位被丢弃,
     实际访问 0FFEFH(地址绕回)—— 这就是 A20 问题的由来。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 以为段基址可以任意值
     段基址必须是 16 的倍数(末尾是 0),因为 ×16 后末尾补了个 0。

  2. 小端模式记反
     低字节在【低】地址。字 1234H 在 1000H 处,则 1000H 是 34H。

  3. 把逻辑地址和物理地址混为一谈
     逻辑地址是 段:偏移(程序里写的),物理地址是 20 位数(总线上跑的)。

  4. 认为一个段固定占 64KB
     段最大 64KB,但实际用多少看你给的偏移范围,可以只用一个字节。

  5. PUSH/POP 时 SP 的加减记反
     PUSH 减 2(向下长),POP 加 2。而且是"先减后存、先取后加"。

  6. 忘记 ×16 后可能超过 1MB
     FFFF:FFFF 就超了。8086 会截断,这就是 A20 问题。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 4 篇(寻址方式):所有寻址方式都是在算"偏移",
      段基址另外给。这一篇的公式是那一篇的地基。
    · 第 5 篇(指令系统):取指时 CS:IP,取数据时 DS:BX,
      写代码时你会一直在算偏移。
    · 第 7 篇(存储器与地址译码):硬件怎么把 20 位地址译成片选信号。
    · 第 8、9 篇(中断):中断向量表就住在这 1KB 里。
    · 第 13 篇(总线与现代微机):分页、保护模式,都是分段的"后代"。

    一句话:这一篇是 8086 最"独门"的东西。
    理解了"两个 16 位凑一个 20 位",后面看任何地址计算都不会再懵。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    位图与分布图已设置避免跨页断开,正文按 10pt 排版。
04 寻址方式
========================================================================
      微机原理与接口技术 · 第 4 篇
      寻址方式 —— 30 分钟速通
========================================================================

前置:第 3 篇(段、偏移、物理地址)。
这一篇回答一个问题:指令里的操作数到底在哪、怎么找到它。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

看三条指令:

        MOV AX, 1234H        ← 1234H 就在指令里
        MOV AX, BX           ← 数在 BX 里
        MOV AX, [BX]         ← 数在"BX 指向的那个地址"里,要去取

    第三条的方括号是关键:它表示"去存储器取"。

    问题是:CPU 怎么知道去哪个地址取?
        这就是"寻址方式"要回答的。

    核心概念是【有效地址 EA】:
        EA 就是偏移量。算出 EA,再加上段基址,才是物理地址。

一共六站:

   第 1 站  操作数可能在哪三个地方                    (4 分钟)
   第 2 站  三种最简单的:立即、寄存器、直接          (5 分钟)
   第 3 站  寄存器间接:第一个"指针"                  (5 分钟)
   第 4 站  相对、基址变址、相对基址变址              (6 分钟)
   第 5 站  合法组合、默认段、段超越                  (5 分钟)
   第 6 站  ★ 寻址方式在机器码里长什么样              (5 分钟)

★★ 本篇的核心数字(都是实算的机器码)★★

    MOV AX, [BX]           8B 07H          (2 字节,最短)
    MOV AX, [BX+SI]        8B 00H
    MOV AX, [1234H]        8B 06 34 12H    (位移低字节在前)
    MOV AX, [BP]           8B 46 00H       (★ 必须多带一个 00!)
    MOV AX, [BX+SI+1234H]  8B 80 34 12H
    合法寄存器组合只有 4 种:BX+SI、BX+DI、BP+SI、BP+DI


========================================================================
  第 1 站(0-4 分钟)  操作数可能在哪三个地方
------------------------------------------------------------------------



    【① 在指令里】叫立即寻址
        指令本身带着这个数,CPU 拿来就用,不用去别处找。
        例:MOV AX, 1234H     →  机器码里有 34 12 两个字节

    【② 在寄存器里】叫寄存器寻址
        指令里写寄存器名,CPU 直接从内部寄存器取,最快。
        例:MOV AX, BX

    【③ 在存储器里】这才是"寻址方式"真正要解决的问题
        指令里给一个"怎么算地址"的规则,CPU 按规则算出地址再去取。
        例:MOV AX, [BX]

    ★ 一句话判断:有方括号才是访存。
      没有方括号的寄存器名,就是寄存器本身。

【为什么存储器寻址这么复杂】

    因为要给程序员灵活性。想想数组和结构体:

        访问数组第 i 个元素:地址 = 数组首址 + i × 2
        访问结构体成员:  地址 = 结构体首址 + 成员偏移 + 数组下标

    这两种需求催生了"基址 + 变址 + 位移"的组合。
    8086 的七种寻址方式,本质上就是这几种组合的不同简化形式。


========================================================================
  第 2 站(4-9 分钟)  三种最简单的寻址方式
------------------------------------------------------------------------

【① 立即寻址:数就在指令里】

        MOV AX, 1234H

    机器码:B8 34 12

         B8      ← 操作码:把立即数送 AX
         34 12   ← 立即数 1234H(低字节在前!)

    ★ 注意:立即数在机器码里也是【小端】的,34 在前、12 在后。

    限制:立即数只能是常数,不能是变量。而且不能直接送给段寄存器
          (必须经过通用寄存器中转)。

【② 寄存器寻址:数在寄存器里】

        MOV AX, BX

    机器码:8B C3

         C3 = 11 000 011B
              mod=11(寄存器寻址)、reg=000(AX)、r/m=011(BX)

    ★ mod=11 是信号:"这次操作数不是存储器,是寄存器"。

    优缺点:最快(不访问存储器),但寄存器只有 14 个,不够用。

【③ 直接寻址:地址直接写在指令里】

        MOV AX, [1234H]

    机器码:8B 06 34 12

         8B      ← 操作码:MOV reg16, r/m16
         06      ← ModR/M:mod=00(无位移)、reg=000(AX)、r/m=110(直接寻址)
         34 12   ← 位移量 1234H(低字节在前)

    物理地址 = DS × 16 + 1234H

    ★ 注意 r/m=110 在 mod=00 时的特殊含义:不是 [BP],而是"直接寻址"。
      这个"占用"导致【[BP] 不能直接编码】,第 6 站会讲。

    优缺点:写起来直观,但地址写死了,不能改。所以实际程序里很少用,
            通常用于访问固定的硬件地址(比如显存 B800H)。


========================================================================
  第 3 站(9-14 分钟)  寄存器间接寻址:第一个"指针"
------------------------------------------------------------------------

【写法】

        MOV AX, [BX]

    方括号里放一个寄存器,意思是:"寄存器里的值就是地址"。

    这是"指针"概念的硬件实现:

        BX = 0300H,DS = 2000H
        → EA = 0300H
        → 物理地址 = 20000H + 0300H = 20300H
        → 从 20300H 取一个字放进 AX

【能放哪几个寄存器】

    只有四个:BX、BP、SI、DI。

    ★ 为什么 AX、CX、DX 不行?
      因为它们没有"地址运算"的硬件通路。
      8086 内部有一个专门算地址的加法器,只有 BX/BP/SI/DI 连到它上面。
      这是硬件决定的,不是随便规定的。

【机器码对比(体会一下有多省)】

        MOV AX, [BX]    8B 07        ← 2 字节
        MOV AX, [SI]    8B 04        ← 2 字节
        MOV AX, [DI]    8B 05        ← 2 字节

    只有 2 个字节!因为地址完全由寄存器给,指令里不用存地址。

    ★ 这就是为什么循环里都用 [SI] 而不是 [1234H]:
      前者只要 2 字节,后者要 4 字节;而且前者只要改 SI 就能换地址。

【用间接寻址写循环(预告第 6 篇)】

        MOV SI, 0           ; 从数组开头开始
        MOV CX, 100         ; 循环 100 次
    L1: MOV AL, [SI]        ; 取一个字节
        INC SI              ; 指针后移
        LOOP L1             ; CX 减 1,不为 0 就跳回 L1

    这就是数组遍历的经典写法,全靠间接寻址。


========================================================================
  第 4 站(14-20 分钟)  加位移、加变址
------------------------------------------------------------------------

【④ 寄存器相对寻址:间接 + 位移】

        MOV AX, [BX+08H]

    EA = (BX) + 08H

    用途:访问结构体的成员(基址固定,成员偏移固定)。

        设 BX 指向一个结构体首地址
        [BX+0]  →  第一个成员
        [BX+2]  →  第二个成员
        [BX+4]  →  第三个成员

    机器码:8B 47 08
        47 = 01 000 111B → mod=01(8 位位移)、reg=000、r/m=111(BX)
        08 → 位移 8

    ★ 位移可以是 8 位(-128~127)或 16 位。编译器会自动选短的。

【⑤ 基址变址寻址:基址 + 变址】

        MOV AX, [BX+SI]

    EA = (BX) + (SI)

    用途:访问二维数组(基址给行、变址给列),或者"数组首址 + 下标"。

        BX 存数组首地址,SI 存下标 ×2
        [BX+SI] 就是第 SI/2 个元素

    机器码:8B 00
        00 = 00 000 000B → mod=00、reg=000、r/m=000(BX+SI)

    ★ 只有 2 个字节,因为两个寄存器都指定了,不用存地址。

    ☆ 顺便说:实现"数组下标"其实一条指令就够 ——
      这正是 8086 比早期 CPU 好用的地方。

【⑥ 相对基址变址:三者都加】

        MOV AX, [BX+SI+1234H]

    EA = (BX) + (SI) + 1234H

    机器码:8B 80 34 12
        80 = 10 000 000B → mod=10(16 位位移)、reg=000、r/m=000(BX+SI)
        34 12 → 位移 1234H

    用途:二维数组 + 结构体(最复杂的情况)。

【七种方式汇总(这张表要会看)】



    记忆方法:
        · 有方括号 = 访存
        · 方括号里是常数 = 直接
        · 方括号里是寄存器 = 间接
        · 再加常数 = 相对
        · 再加一个寄存器 = 基址变址
        · 再加常数 = 相对基址变址

    ★ 就是"叠加"关系,从简单往复杂一层层加。


========================================================================
  第 5 站(20-25 分钟)  合法组合与默认段
------------------------------------------------------------------------

【不是任意两个寄存器都能配】

    规则:一个基址 + 一个变址。

        基址(base):BX、BP      ← 决定用哪个段
        变址(index):SI、DI     ← 只是加个数

    所以合法的只有 4 种:

        BX+SI    BX+DI    BP+SI    BP+DI

    非法的:

        BX+BP   ✗  两个都是基址
        SI+DI   ✗  两个都是变址
        AX+SI   ✗  AX 不能进方括号



【为什么两个基址不行(这是个好问题)】

    因为基址决定"用哪个段":
        BX → DS
        BP → SS

    如果 BX+BP 一起用,那到底该用 DS 还是 SS?
    硬件没法回答,所以干脆不允许。

    ★ 而变址寄存器(SI/DI)不决定段,只负责加一个数,
      所以可以随便和任何一个基址配。

【默认段搭配(背下来)】

        含 BP 或 SP 的  →  默认 SS(堆栈段)
        其它所有情况    →  默认 DS(数据段)

    具体:
        [BX] [SI] [DI] [BX+SI] [BX+DI] [1234H]  →  DS
        [BP] [BP+SI] [BP+DI] [SP]               →  SS

    ★ 记忆法:BP 是 Basement Pointer(堆栈基址),它总在堆栈段里。

【想改段怎么办:段超越前缀】

    在方括号前加"段寄存器:",这叫段超越:

        MOV AX, ES:[BX]      ← 用 ES 段,不用 DS
        MOV AX, SS:[BX]      ← 用 SS 段
        MOV AX, CS:[BX]      ← 用 CS 段
        MOV AX, DS:[BP]      ← 用 DS 段(覆盖 BP 默认的 SS)

    每个前缀在机器码里占 1 个字节:

        ES: → 26H      CS: → 2EH      SS: → 36H      DS: → 3EH

    ★ 所以加了段超越,指令就变长 1 个字节。

    什么时候要用?
        · 数据不在默认段里(比如在附加段 ES 里做串拷贝)
        · 访问显存(B800H 段)时想明确指定


========================================================================
  第 6 站(25-30 分钟)  ★ 寻址方式在机器码里长什么样
------------------------------------------------------------------------

    这一站是"想深入"的部分。不考试也能看懂,但看懂了你会对
    "指令"有完全不同的理解。

【核心:ModR/M 字节】

    8086 的访存指令,第 2 个字节叫 ModR/M,它把寻址方式编码进去了:

        bit7 bit6 | bit5 bit4 bit3 | bit2 bit1 bit0
          mod     |      reg       |      r/m

    mod(2 位):有没有位移
        00  无位移
        01  跟 8 位位移
        10  跟 16 位位移
        11  寄存器寻址(r/m 是寄存器号)

    reg(3 位):用哪个寄存器
        000 AX  001 CX  010 DX  011 BX
        100 SP  101 BP  110 SI  111 DI

    r/m(3 位):存储器操作数怎么算 EA
        000 BX+SI    001 BX+DI    010 BP+SI    011 BP+DI
        100 SI       101 DI       110 BP(或直接寻址)   111 BX



【逐条解读机器码】

        MOV AX, [BX]      8B 07
            07H = 00 000 111B
            mod=00(无位移)、reg=000(AX)、r/m=111(BX)✓

        MOV AX, [BX+SI]   8B 00
            00H = 00 000 000B → r/m=000 就是 BX+SI ✓

        MOV AX, [1234H]   8B 06 34 12
            06H = 00 000 110B → r/m=110,mod=00
            ★ 这个组合不是 [BP],而是【直接寻址】,后面跟 16 位地址 ✓

        MOV AX, [BX+SI+1234H]  8B 80 34 12
            80H = 10 000 000B → mod=10(16 位位移)、r/m=000(BX+SI)✓

    ★ 全部对得上,说明你对寻址方式的理解是准确的。

【★ 一个著名的特例:[BP] 为什么要多一个字节】

        想表达 MOV AX, [BP](用 BP 指地址,不带位移)
        mod 应该是 00,r/m 应该是 110(BP)
        → ModR/M = 00 000 110 = 06H
        → 机器码应该是 8B 06

        但 8B 06 已经被【直接寻址】占用了!
        因为 mod=00 + r/m=110 这个组合被规定给"直接寻址"(跟 16 位地址)。

        所以 [BP] 只能退一步,用 mod=01(8 位位移)+ 位移 00:

        MOV AX, [BP]  实际编码成  8B 46 00
            46H = 01 000 110B → mod=01、r/m=110(BP)、位移 00H

    ★ 结论:[BP] 比 [BX] 多占 1 个字节。
      这是 8086 编码里的一个"历史遗留",考试偶尔会考。

【完整计算链】



    从指令到数据,一共四步:

        ① 指令给出寻址方式(比如 [BX+SI+100H])
        ② 算出 EA = BX + SI + 100H = 0600H
        ③ 物理地址 = DS×16 + EA = 20000H + 0600H = 20600H
        ④ 去 20600H 取数据

    ★ 第 ② 步的 EA 只有 16 位(最大 FFFFH),所以一个段最多 64KB ——
      这就是"段最大 64KB"的真正来源。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 一条指令的操作数可能在哪三个地方?
  2. 怎么快速判断一条指令是不是要访问存储器?
  3. 立即寻址的立即数在机器码里怎么存放?举个例子。
  4. 哪些寄存器可以放在方括号里?为什么只有它们?
  5. 写出下列指令的 EA 公式:
     (a) MOV AX,[SI]   (b) MOV AX,[BX+4]   (c) MOV AX,[BP+DI+8]
  6. 合法的寄存器组合有哪 4 种?为什么 BX+BP 不行?
  7. [BX] 和 [BP] 默认分别用哪个段?为什么?
  8. 想让 [BX] 用 ES 段,怎么写?前缀字节是多少?
  9. DS=2000H、BX=0300H、SI=0200H,MOV AX,[BX+SI+100H] 的物理地址?
 10. MOV AX,[BX] 的机器码是 8B 07H,请把 07H 拆成 mod/reg/r/m 并解释。
 11. 为什么 MOV AX,[BP] 的机器码是 8B 46 00H,而不是 8B 06H?
 12. 为什么说"段最大 64KB"?这个限制来自哪里?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 指令中(立即数)、寄存器中、存储器中。
  2. 看有没有方括号。有方括号就是要访问存储器。
  3. 低字节在前(小端)。MOV AX,1234H 的机器码是 B8 34 12H。
  4. 只有 BX、BP、SI、DI。因为它们连在专门算地址的加法器上,
     AX/CX/DX 没有这条硬件通路。
  5. (a) EA = (SI)
     (b) EA = (BX) + 4
     (c) EA = (BP) + (DI) + 8
  6. BX+SI、BX+DI、BP+SI、BP+DI。
     BX+BP 不行,因为两个都是基址,而基址决定用哪个段(BX→DS、BP→SS),
     两个一起用就定不下段了。
  7. [BX] 默认 DS,[BP] 默认 SS。因为 BP 是堆栈基址指针,
     它专门用来访问堆栈里的数据。
  8. MOV AX, ES:[BX];前缀字节 26H。
  9. EA = 0300H + 0200H + 0100H = 0600H;
     物理地址 = 20000H + 0600H = 20600H。
 10. 07H = 0000 0111B → mod=00(无位移)、reg=000(AX)、
     r/m=111(BX)。所以是"用 BX 的值当地址,取一个字送 AX"。
 11. 因为 mod=00 且 r/m=110 这个组合被【直接寻址】占用了
     (8B 06 34 12 表示 [1234H])。所以 [BP] 只能用
     mod=01 + 8 位位移 00,编码成 8B 46 00H(多 1 个字节)。
 12. 因为 EA(偏移)只有 16 位,最大 FFFFH = 65535,
     所以一个段内最多寻址 64KB。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 把 [BX] 和 BX 搞混
     [BX] 是"BX 指向的那个存储器单元",BX 是"BX 里的数本身"。

  2. 以为任意两个寄存器都能放方括号里
     只有"一个基址 + 一个变址"的 4 种组合合法。

  3. 忘了 BP 默认用 SS 段
     用 BP 访问数据段时必须加 DS: 前缀,否则取错地方。

  4. 位移量字节序记反
     16 位位移也是低字节在前:1234H 在机器码里是 34 12。

  5. 以为 [BP] 和 [BX] 一样短
     [BP] 多 1 个字节(8B 46 00 vs 8B 07)。

  6. 把"偏移"和"物理地址"混为一谈
     EA 是偏移(16 位),物理地址是 20 位。中间还要加段基址。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 5 篇(指令系统):每条指令都要指定操作数的寻址方式,
      这一篇的 7 种方式会反复出现。
    · 第 6 篇(汇编程序):数组遍历用 [SI],结构体用 [BX+偏移],
      二维数组用 [BX+SI],全靠这一篇。
    · 第 7 篇(存储器译码):硬件怎么把 20 位地址变成片选信号。
    · 第 10 篇(8255A):端口地址也要寻址,不过用的是 I/O 指令的
      16 位端口地址,不经过段机制。

    一句话:这一篇是"怎么找到数"的完整答案。
    重点记三样:EA 的七种算法、4 种合法组合、默认段搭配。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    对照表与位图已设置避免跨页断开,正文按 10pt 排版。
05 数据传送与算术运算
========================================================================
      微机原理与接口技术 · 第 5 篇
      指令系统(一):数据传送与算术运算 —— 30 分钟速通
========================================================================

前置:第 4 篇(寻址方式)。这一篇开始真正"写指令"了。
这一篇只讲两件事:怎么搬数据,怎么算数。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

8086 有一百多条指令,听起来吓人。但按功能分只有五类:



这一篇先啃前两类,因为它们是"砖头":
任何程序,八成以上的指令都是"搬"和"算"。

一共六站:

   第 1 站  指令长什么样:机器码的结构          (4 分钟)
   第 2 站  MOV 与数据传送                      (6 分钟)
   第 3 站  加减法:ADD/SUB/CMP/NEG             (5 分钟)
   第 4 站  ★ CMP 与条件转移(有符号 vs 无符号)(6 分钟)
   第 5 站  乘除法与符号扩展                    (5 分钟)
   第 6 站  BCD 调整与综合练习                  (4 分钟)

★★ 本篇的核心数字(机器码全是按编码规则算出来的)★★

    MOV AX, 1234H         B8 34 12H     立即数低字节在前
    MOV AX, BX            8B C3H        mod=11 表示寄存器
    XCHG AX, BX           93H           ★ 特例:90H + 寄存器号
    LEA AX, [BX+SI]       8D 00H
    CMP AX, BX            3B C3H        (做了减法但不存结果)
    MUL BL                F6 E3H        F6 /4
    DIV BL                F6 F3H        F6 /6
    CBW                   98H           符号扩展到 AX
    CWD                   99H           符号扩展到 DX:AX


========================================================================
  第 1 站(0-4 分钟)  指令长什么样
------------------------------------------------------------------------

【一条指令 = 操作码 + 操作数】

        MOV  AX,  BX
        ↑    ↑     ↑
        操作码 目的  源

    机器码:8B C3

        8B  ← 操作码:告诉 CPU「这是一条 MOV,方向是 reg←r/m」
        C3  ← ModR/M:11 000 011
                       mod=11(寄存器)、reg=000(AX)、r/m=011(BX)

    ★ 第 4 篇讲的 ModR/M,就是"操作数在哪"的编码。

【指令长度不固定:1 ~ 6 个字节】

        XCHG AX, BX      93                 1 字节
        INC AX           40                 1 字节
        MOV AX, BX       8B C3              2 字节
        MOV AX, [BX]     8B 07              2 字节
        MOV AX, 1234H    B8 34 12           3 字节
        MOV AX, [1234H]  8B 06 34 12        4 字节
        MOV AX, [BX+SI+1234H]  8B 80 34 12  4 字节

    ★ 变长指令的好处:常用的(寄存器之间)短,少用的(复杂寻址)长,
      平均下来代码更紧凑。代价是译码复杂。

【立即数和位移都是"低字节在前"】

        MOV AX, 1234H  →  B8 34 12
                              ↑  ↑
                             低  高

    这就是第 3 篇讲的小端模式,指令里的数也一样。


========================================================================
  第 2 站(4-10 分钟)  MOV 与数据传送
------------------------------------------------------------------------

【MOV:最基础的搬运工】

        MOV  目的, 源

    注意方向:目的在前,源在后。和我们平时说"把源送到目的"是反的,
    所以初学时容易写反。

【★ 三条铁律(考试最爱考)】



    ① 不能【存储器 → 存储器】
       想搬?必须经过寄存器中转:
           MOV AL, [SI]      ; 先从内存取到 AL
           MOV [DI], AL      ; 再从 AL 存到内存

    ② 不能把立即数直接送【段寄存器】
       必须经过通用寄存器:
           MOV AX, 2000H
           MOV DS, AX

       ★ 这是初学者最常犯的错:MOV DS, 2000H 是非法的。

    ③ 不能【段寄存器 → 段寄存器】,且 CS 永远不能当目的
       CS 只能用 JMP FAR / CALL FAR / RETF / INT 改。

【MOV 的机器码家族(了解规律即可)】

    88 /r    MOV r/m8,  r8      从寄存器存到内存/寄存器
    8A /r    MOV r8,  r/m8      从内存/寄存器取到寄存器
    89 /r    MOV r/m16, r16
    8B /r    MOV r16, r/m16
    B0+reg   MOV r8,  imm8      立即数送 8 位寄存器
    B8+reg   MOV r16, imm16     立即数送 16 位寄存器

    ★ 88/89 是"往外写",8A/8B 是"往里读"。
      这就是为什么 MOV AX,BX 是 8B C3(读 BX)、
      而 MOV BX,AX 是 8B D8(读 AX,reg=011 是 BX)。

【XCHG:交换,有个大特例】

        XCHG AX, BX

    机器码:93

    ★ 特例:XCHG AX, r16 编码成 90H + 寄存器号
      (AX,AX)=90、(AX,CX)=91、(AX,DX)=92、(AX,BX)=93

    为什么?因为用一个字节就够了,省空间。
    (90H 单独一条 XCHG AX,AX 就是那个著名的 NOP 指令)

【LEA:只算地址,不取数据】

        LEA AX, [BX+SI]

    机器码:8D 00

    作用:把 [BX+SI] 这个【地址】算出来,放进 AX。
          它不访问存储器,只是做加法!

    ★ LEA 和 MOV 的区别(常考):
        LEA AX, [BX]        ; AX = BX(把地址值给 AX)
        MOV AX, [BX]        ; AX = 内存[BX] 里的内容(要去取)

    LEA 的妙用:可以做三次加法(BX+SI+位移),比用 ADD 两次还快。

【PUSH / POP:压栈出栈】

        PUSH AX      50H      (50H + 寄存器号,AX=0)
        PUSH BX      53H
        POP  AX      58H      (58H + 寄存器号)

    一次压入/弹出【两个字节】(一个字)。

    ★ 记住第 3 篇的:PUSH 先减 2 再存,POP 先取再加 2。

【IN / OUT:和端口打交道(第 8 篇细讲)】

        IN   AL, 60H      ; 从 60H 端口读一个字节到 AL
        OUT  60H, AL      ; 把 AL 写到 60H 端口

    ★ 端口地址 ≤ 255 时可以直接写;> 255 必须用 DX:
        MOV DX, 3F8H
        IN  AL, DX

    ★ 注意:这是 I/O 端口,不是存储器地址。8086 有两套独立的地址空间
      (存储器 1MB,I/O 64KB),靠 M/IO 引脚区分。


========================================================================
  第 3 站(10-15 分钟)  加减法
------------------------------------------------------------------------

【基本四条】

        ADD  目的, 源     目的 = 目的 + 源
        ADC  目的, 源     目的 = 目的 + 源 + CF    (带进位加)
        SUB  目的, 源     目的 = 目的 − 源
        SBB  目的, 源     目的 = 目的 − 源 − CF    (带借位减)

    为什么要 ADC/SBB?为了做【多字节运算】。

        例:两个 32 位数相加(8086 一次只能算 16 位)
            低 16 位用 ADD(会置 CF)
            高 16 位用 ADC(把低位的进位加进来)

【INC / DEC:加减 1】

        INC AX      40H      (40H + 寄存器号)
        DEC AX      48H

    ★ 重要区别:INC/DEC 不影响 CF!
      而 ADD AX,1 会影响 CF。
      在循环里用 INC SI 而不担心破坏进位标志,就是因为这个。

【CMP:比较(本质是做减法)】

        CMP AX, BX     3B C3H

    ★ 它做一次 AX − BX,但结果不保存,只更新标志位。

    这是本章最重要的指令,因为它要和条件转移配合(下一站)。

【NEG:取负(求补)】

        NEG AX     F7 D8H      (F7 /3)

    作用:AX = 0 − AX,等价于"取反加一"。

    ★ 用途:把加法变减法。A − B 可以写成 A + (−B)。
      另外 NEG 后 CF 的值有特殊含义:AX≠0 时 CF=1。

【标志位怎么变(四条规律)】

    ① ZF:结果为 0 时置 1(判断相等就靠它)
    ② SF:结果的最高位(就是"看起来是不是负数")
    ③ CF:无符号数有没有进位/借位
    ④ OF:有符号数有没有溢出(正+正得负 或 负+负得正)

【实算几个】

        ADD AL, BL     AL=05H, BL=03H → 结果 08H
                       标志位:全部为 0(没有进位、不是 0、非负)

        ADD AL, BL     AL=7FH, BL=01H → 结果 80H
                       标志位:AF=1、SF=1、OF=1
                       (127+1=128 超出 8 位有符号范围)

        ADD AL, BL     AL=FFH, BL=01H → 结果 00H
                       标志位:CF=1、PF=1、AF=1、ZF=1
                       (无符号进位;结果恰好为 0)

    ★ 对比后两个例子:都是"最高位变了",但一个 OF=1(有符号溢出),
      一个 CF=1(无符号进位)。这就是 CF 和 OF 的区别。


========================================================================
  第 4 站(15-21 分钟)  ★ CMP 与条件转移
------------------------------------------------------------------------

    这一站是汇编里用得最多、也最容易错的地方。

【CMP 的用法模式】

        CMP  A, B
        JE   相等时去哪        ; ZF=1 说明 A=B
        JG   大于时去哪        ; 有符号
        JB   小于时去哪        ; 无符号

【关键洞察:有符号和无符号用不同的标志位】



        有符号数比较:看 SF、OF、ZF
            小于 → SF ≠ OF
            大于 → SF = OF 且 ZF = 0

        无符号数比较:看 CF、ZF
            小于 → CF = 1
            大于 → CF = 0 且 ZF = 0

    ★ 为什么不一样?因为"大小"的定义不同:
      无符号数 0FFH 是 255(比 1 大);
      有符号数 0FFH 是 −1(比 1 小)。

      硬件只做一次减法,两种解释靠不同的标志位。

【实算:AL=03H, BL=07H,CMP AL,BL 之后】

        AL − BL = 03H − 07H = FCH(即 −4)

        ZF = 0(不是 0)
        SF = 1(结果最高位是 1,看起来是负数)
        OF = 0(3−7 没有溢出)
        CF = 1(3 减 7 需要借位)

        判断:
            有符号:SF(1) ≠ OF(0) → 小于 → JL 成立 ✓(3 < 7)
            无符号:CF = 1 → 小于 → JB 成立 ✓(3 < 7)

        两者结论一样,但走的是不同标志位。

【再看一个会分歧的例子】

        设 AL = 0FFH(无符号 255,有符号 −1)、BL = 01H

        AL − BL = FFH − 01H = FEH(即 −2)

        SF = 1、OF = 0、CF = 0

        有符号:−1 < 1 → 小于 → JL 成立
        无符号:255 > 1 → 大于 → JA 成立

    ★ 同一个 CMP,结论完全相反!
      所以做题时必须先确定数据是有符号还是无符号。
      这是本章最容易掉进去的坑。

【条件转移指令速查(常用 12 条)】

        JE / JZ    相等 / 为零          ZF=1
        JNE / JNZ  不等 / 不为零        ZF=0
        JA / JNBE  高于(无符号)       CF=0 且 ZF=0
        JAE / JNB  高于等于(无符号)   CF=0
        JB / JNAE  低于(无符号)       CF=1
        JBE / JNA  低于等于(无符号)   CF=1 或 ZF=1
        JG / JNLE  大于(有符号)       SF=OF 且 ZF=0
        JGE / JNL  大于等于(有符号)   SF=OF
        JL / JNGE  小于(有符号)       SF≠OF
        JLE / JNG  小于等于(有符号)   ZF=1 或 SF≠OF

    ★ 记忆法:A/B 是无符号(Above/Below),G/L 是有符号(Greater/Less)。


========================================================================
  第 5 站(21-26 分钟)  乘除法与符号扩展
------------------------------------------------------------------------

    乘除法最容易记混的是"结果放在哪"。看这张图就清楚了。



【乘法】

        MUL  源       无符号乘(F6 /4 或 F7 /4)
        IMUL 源       有符号乘(F6 /5 或 F7 /5)

        8 位:AL × 源 → AX(AH 是高 8 位,AL 是低 8 位)
        16 位:AX × 源 → DX:AX

    实算:
        AL=5, BL=3     → AX = 15(AH=00H, AL=0FH),CF=OF=0
        AL=20, BL=13   → AX = 260(AH=01H, AL=04H),CF=OF=1

    ★ CF 和 OF 的含义:如果结果的高半部分(AH 或 DX)不为 0,
      说明结果"放不进"半宽里,CF=OF=1。这用于判断是否需要扩宽。

【除法】

        DIV  源       无符号除(F6 /6 或 F7 /6)
        IDIV 源       有符号除(F6 /7 或 F7 /7)

        8 位:AX ÷ 源 → AL = 商、AH = 余数
        16 位:DX:AX ÷ 源 → AX = 商、DX = 余数

    实算:
        AX=100, BL=7   → AL=0EH(商 14)、AH=02H(余 2)

    ★ 两个大坑:
      ① 除法前必须把 DX/AH 清零(有符号要先 CWD),
         否则会算出荒唐的结果。
      ② 如果商超过 FFH(8 位)或 FFFFH(16 位),
         CPU 会产生【除法错中断(类型 0)】,程序直接崩。

         例:AX=4660 ÷ 7 = 665,超过 255 → 触发类型 0 中断。

【符号扩展:CBW / CWD】

    8 位负数要做 16 位运算前,必须先"补符号位"。

        CBW    AL → AX      (98H)
        CWD    AX → DX:AX   (99H)



    CBW 实算:
        AL = 05H(+5)  →  AX = 0005H(AH 填 00H)
        AL = FBH(−5)  →  AX = FFFBH(AH 填 FFH)
        AL = 80H(−128)→  AX = FF80H(AH 填 FFH)

    ★ 一句话:符号扩展 = 用符号位填高位。
      补 0 会把 −5 变成 +251,这是致命的错误。

    什么时候必须用?

        MOV AL, BL
        CBW              ; ← 少了这一条,负数就全错了
        MOV CL, 10
        IDIV CL

    标准写法(有符号除法前):

        MOV AL, [SI]
        CBW              ; AL → AX,补好符号
        MOV CL, [DI]
        IDIV CL          ; AX ÷ CL → AL 商、AH 余


========================================================================
  第 6 站(26-30 分钟)  BCD 调整与综合练习
------------------------------------------------------------------------

【BCD 调整四条】

        DAA   (27H)   ADD 之后调整,用于加法
        DAS   (2FH)   SUB 之后调整,用于减法
        AAA   (37H)   ADD 之后调整,用于 ASCII 数字
        AAS   (3FH)   SUB 之后调整

    实算 BCD 加法:
        25H + 37H = 5CH(低半字节 C>9,必须调整)
        DAA:先加 06H → 62H(高半字节 6 ≤ 9,不用再加 60H)
        结果 62H = 十进制 62 ✓(25 + 37 = 62)

    实算 BCD 减法:
        42H − 17H = 2BH(低半字节 2−7 需要借位,AF=1)
        DAS:先减 06H → 25H
        结果 25H = 十进制 25 ✓(42 − 17 = 25)

    ★ 用法必须是紧跟在加减指令后面,中间不能插入别的指令
      (因为要看这一条产生的 AF 和 CF)。

【综合练习一:写两个 32 位数相加】

    设第一个数存在 DS:SI 指向的 4 个字节,
    第二个数存在 DS:DI 指向的 4 个字节。
    把和存回 SI 指向的地方。

        MOV  AX, [SI]        ; 低 16 位
        ADD  AX, [DI]        ; 相加,可能产生进位 CF
        MOV  [SI], AX

        MOV  AX, [SI+2]      ; 高 16 位
        ADC  AX, [DI+2]      ; ★ 用 ADC 把低位的进位加进来
        MOV  [SI+2], AX

    ★ 关键就是那个 ADC。用 ADD 就会丢掉进位。

【综合练习二:判断一个数是正、负还是零】

        CMP  AL, 0
        JE   IS_ZERO         ; ZF=1 → 是 0
        JS   IS_NEG          ; SF=1 → 是负数
        ; 否则是正数
        JMP  DONE
    IS_NEG:
        ; 负数处理
        JMP  DONE
    IS_ZERO:
        ; 零处理
    DONE:

    ★ 注意这里用的是 JS(SF=1),因为比较的对象是 0,
      不需要考虑溢出问题。

【综合练习三:无符号数取绝对值】

        MOV  AL, [SI]
        CMP  AL, 80H
        JB   POSITIVE        ; 小于 80H → 是"正数"(无符号意义上)
        NEG  AL              ; 0 − AL,取补
    POSITIVE:
        MOV  [DI], AL

    ★ 这里必须用 JB(无符号),因为我们要判断的是"最高位是不是 1"。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 一条指令由哪两部分组成?指令长度为什么是变长的?
  2. MOV 的三条铁律是什么?
  3. 想把 [SI] 的内容搬到 [DI],怎么写?为什么不能一条指令搞定?
  4. XCHG AX, BX 的机器码是多少?为什么这么短?
  5. LEA AX,[BX] 和 MOV AX,[BX] 有什么区别?
  6. INC 和 ADD AX,1 有什么重要区别?
  7. ADC 和 SBB 是干什么用的?为什么需要它们?
  8. CMP 的本质是什么?它保存运算结果吗?
  9. 有符号比较看哪些标志位?无符号比较看哪些?
 10. MUL BL 的结果放在哪?如果 AH≠0 会怎样?
 11. DIV BL 之前为什么要把 AH 清零?不清零会有什么后果?
 12. CBW 和 CWD 分别做什么?为什么负数必须用它们?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 操作码 + 操作数。变长是为了让常用指令短、少用指令长,
     平时代码更紧凑(代价是译码复杂)。
  2. ① 不能存储器→存储器;② 不能立即数→段寄存器;
     ③ 不能段寄存器→段寄存器,且 CS 不能当目的。
  3. MOV AL,[SI] 然后 MOV [DI],AL。因为 8086 不支持存储器到存储器
     直接传送,必须经过寄存器中转。
  4. 93H。因为 XCHG AX,r16 有专门编码:90H + 寄存器号,BX 是 3。
  5. LEA 只算地址(把 BX 的值给 AX),不访问存储器;
     MOV 会去 BX 指向的地址取数据。LEA 很快,常用来做加法。
  6. INC 不影响 CF,ADD AX,1 会影响 CF。
     循环里用 INC 不会破坏进位标志。
  7. 带进位加、带借位减。用于多字节运算:低位用 ADD/SUB 产生进位,
     高位用 ADC/SBB 把进位算进来。
  8. 做一次减法(目的 − 源),但结果不保存,只更新标志位。
  9. 有符号:SF、OF、ZF(小于看 SF≠OF)。
     无符号:CF、ZF(小于看 CF=1)。
 10. 8 位乘法的结果放在 AX(AH 高 8 位、AL 低 8 位)。
     AH≠0 说明结果超出 8 位,此时 CF=OF=1。
 11. 因为 DIV 是"AX ÷ 源",如果 AH 有垃圾值,被除数就不是你想要的那个数,
     会算出错误结果或商溢出。有符号除法要先 CWD。
     商超过 FFH 会触发除法错中断(类型 0)。
 12. CBW:AL 符号扩展成 AX;CWD:AX 符号扩展成 DX:AX。
     负数高位必须补 1,补 0 会变成很大的正数。


========================================================================
  本篇最容易犯的 7 个错
========================================================================

  1. MOV 目的,源 的方向写反
     目的在前。MOV AX,BX 是把 BX 给 AX。

  2. MOV DS, 2000H
     非法!立即数不能直接进段寄存器,要经过 AX 中转。

  3. 忘了用 ADC/SBB 做多字节运算
     高位必须用带进位的指令,否则低位进位丢失。

  4. 有符号和无符号的判断混用
     有符号用 JL/JG(看 SF/OF),无符号用 JB/JA(看 CF)。
     0FFH 在两个体系里大小完全相反。

  5. 除法前忘了清 DX/AH 或 CWD
     不清零 → 结果错;不 CWD → 负数全错。

  6. 以为 INC 会影响 CF
     不会。只有 ADD/SUB 系列才影响 CF。

  7. BCD 调整指令中间插了别的指令
     DAA/DAS 必须紧跟在 ADD/SUB 后面,因为要看那一条产生的标志位。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 6 篇(汇编程序):所有程序都是"搬 + 算 + 判断 + 跳",
      这一篇给了"搬"和"算",下一篇给"判断"和"跳"。
    · 第 7 篇(存储器):地址用 LEA 或 MOV 配合寻址方式算出。
    · 第 8 篇(I/O):IN/OUT 指令在这一篇只提了一句,那里细讲。
    · 第 10 篇(8255A):端口操作全靠 IN/OUT。

    一句话:这一篇是"动词表"。学完你就知道 CPU 能干哪些动作了,
    下一篇教你怎么把这些动作串成程序。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    对照表与矩阵已设置避免跨页断开,正文按 10pt 排版。
06 逻辑移位与汇编程序设计
========================================================================
      微机原理与接口技术 · 第 6 篇
      逻辑运算、移位、串操作与汇编程序设计 —— 30 分钟速通
========================================================================

前置:第 5 篇(数据传送与算术)。这一篇补齐剩下的指令,
并且把它们串成一个真正能跑起来的完整程序。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

第 5 篇学了"搬"和"算",但程序还缺两样东西:
    · 按位处理的能力(逻辑运算、移位)
    · 改变执行流程的能力(跳转、循环、调用)

补齐这两样,就能写程序了。

一共六站:

   第 1 站  逻辑运算:AND / OR / XOR / NOT / TEST    (4 分钟)
   第 2 站  ★ 八种移位:区别只在"补什么"            (6 分钟)
   第 3 站  串操作:一次搬一串                        (4 分钟)
   第 4 站  控制转移:跳转、循环、调用                (5 分钟)
   第 5 站  汇编程序的完整结构                        (6 分钟)
   第 6 站  ★ 一个能跑的完整程序(逐条讲解)        (5 分钟)

★★ 本篇的核心数字 ★★

    AND AX,BX    23 C3H      OR  AX,BX    0B C3H
    XOR AX,BX    33 C3H      TEST AX,BX   85 C3H
    NOT AX       F7 D0H

    AL = B4H 移 1 位的结果:
        SHL → 68H(CF=1)      SHR → 5AH(CF=0)
        SAR → DAH(CF=0)      ROL → 69H(CF=1)
        ROR → 5AH(CF=0)

    移位机器码:D1 C0(ROL)/ D1 E0(SHL)/ D1 E8(SHR)/ D1 F8(SAR)


========================================================================
  第 1 站(0-4 分钟)  逻辑运算
------------------------------------------------------------------------

【核心思想:逐位独立,互不影响】

    算术运算有进位(第 0 位加到第 1 位),逻辑运算【没有】。
    每一位各算各的,这就是"位操作"。



【五条指令】

        AND  目的,源     按位与(全 1 才 1)
        OR   目的,源     按位或(有 1 就 1)
        XOR  目的,源     按位异或(不同才 1)
        NOT  目的        按位取反(单操作数)
        TEST 目的,源     按位与,但只影响标志位,不保存结果

    机器码(AX, BX):
        AND AX,BX   23 C3H
        OR  AX,BX   0B C3H
        XOR AX,BX   33 C3H
        TEST AX,BX  85 C3H
        NOT AX      F7 D0H

【三个必须会用的技巧】

    ① 清零:XOR AX, AX
       为什么不用 MOV AX,0?因为 MOV 要 3 个字节,XOR 只要 2 个,
       而且 XOR 不访问存储器,更快。
       ★ 这是汇编里的经典写法,看到别奇怪。

    ② 判断某一位:TEST AL, 01H
       TEST 做 AND 但不存结果,只看 ZF:
           ZF = 1  →  bit0 是 0
           ZF = 0  →  bit0 是 1
       ★ 比 AND AL,01H 好:AND 会破坏 AL 的原值,TEST 不会。

    ③ 置位 / 清位
           置 bit7 为 1:OR  AL, 80H
           清 bit7 为 0:AND AL, 7FH
       这就是控制硬件寄存器的基本手法(第 10 篇 8255A 会用)。

【★ NOT 不影响任何标志位】

    这是个容易错的地方:NOT 纯粹是位取反,不更新 FLAGS。
    而 NEG(取负,第 5 篇)会影响标志位。

    ★ 一句话:NOT 是"逻辑取反",NEG 是"算术取负",别混。


========================================================================
  第 2 站(4-10 分钟)  ★ 八种移位
------------------------------------------------------------------------

    这一站最关键的一句话:它们的区别只在"补进来的位是什么"。

    先约定:把 8 位看成     [b7][b6][b5][b4][b3][b2][b1][b0]
    往左移 = 往 b7 方向挤,往右移 = 往 b0 方向挤。
    挤出去的那一位进 CF(进位标志)。

【逻辑移位:补 0】

        SHL / SAL   左移,低位补 0,最高位进 CF
        SHR         右移,高位补 0,最低位进 CF

    拿 AL = B4H = 1011 0100B 试一下:

        SHL AL,1    1011 0100 → 0110 1000 = 68H,CF=1(原来最高位是 1)
        SHR AL,1    1011 0100 → 0101 1010 = 5AH,CF=0(原来最低位是 0)

    ★ SHL 和 SAL 是同一条指令(编码完全一样),
      因为左移对无符号和有符号的效果相同。

    ★ 移位的算术意义:
        SHL 1 位 = 乘 2
        SHR 1 位 = 除以 2(无符号)
      比用 MUL/DIV 快得多,编译器会把 ×2、÷2 优化成移位。

【算术移位:补符号位】

        SAR   右移,高位补【符号位】(而不是 0)

    这是唯一"保持负号"的右移:

        SAR AL,1    1011 0100 → 1101 1010 = DAH,CF=0

    验证:B4H 有符号是 −76,右移 1 位应该得 −38。
          DAH 有符号是 −38 ✓

    ★ SHR 和 SAR 的区别(考试必考):

        SHR B4H,1 → 5AH   (高位补 0,把负数当无符号数右移)
        SAR B4H,1 → DAH   (高位补 1,保持负号)

      用 SHR 处理负数会得到荒唐的结果。

【循环移位:挤出去的位又补回来】

        ROL   左移,最高位同时进 CF 和最低位
        ROR   右移,最低位同时进 CF 和最高位
        RCL   左移,经 CF 中转(CF 的旧值补进最低位)
        RCR   右移,经 CF 中转(CF 的旧值补进最高位)

    ROL AL,1    1011 0100 → 0110 1001 = 69H,CF=1
                (最高位 1 挤出去,既进 CF 又回到最低位)

    ROR AL,1    1011 0100 → 0101 1010 = 5AH,CF=0
                (最低位 0 挤出去,既进 CF 又回到最高位)



【RCL / RCR:把 CF 当成第 9 位】

    RCL 相当于把 [CF][b7...b0] 当成一个 9 位的循环移位。
    用途:多字节数的移位(比如 32 位数左移 1 位,
          低 16 位用 SHL,高 16 位用 RCL 把低位挤出的那一位接上)。

    实算(设 CF=0):
        RCL AL,1    1011 0100 → 0110 1000 = 68H,CF=1
        RCR AL,1    1011 0100 → 0101 1010 = 5AH,CF=0

【移位数怎么给】

        SHL AL, 1    移 1 位(编码 D1 /4)
        SHL AL, CL   移 CL 位(编码 D3 /4)
        MOV CL, 4    要先放进 CL
        SHL AL, CL

    ★ 8086 只能移 1 位或移 CL 位,不能直接写 SHL AL,4!
      (后来的 80186 才支持立即数)
      要移 4 位就得先 MOV CL,4。

【各种移位的机器码】

        ROL  D1 C0H    ROR  D1 C8H
        RCL  D1 D0H    RCR  D1 D8H
        SHL  D1 E0H    SHR  D1 E8H    SAR  D1 F8H

    规律:D1 是"移 1 位",reg 字段表示哪种移位(0~7)。
    把 D1 换成 D3 就是"移 CL 位"。


========================================================================
  第 3 站(10-14 分钟)  串操作
------------------------------------------------------------------------

【串操作是什么】

    一次处理一整串数据,而且自动调整指针。不用自己写循环,
    也不用自己 INC SI。

    默认约定(背下来):
        源串:DS:SI
        目的串:ES:DI
        次数:CX

【五条串指令】

        MOVS   搬(源 → 目的)
        CMPS   比较两串
        SCAS   在串里找字符(与 AL/AX 比)
        LODS   从串取到 AL/AX
        STOS   把 AL/AX 存进串

    每条都有三个版本,靠后缀指定宽度:
        MOVSB(字节)、MOVSW(字)



【★ REP 前缀:一句话搞定循环】

        REP MOVSB

    含义:重复执行 MOVSB 共 CX 次,每次自动:
        SI ± 1、DI ± 1、CX − 1
    直到 CX = 0。

    ★ 这就是"批量搬运"的最快写法,比手写 LOOP 循环还短。

    其他前缀:
        REPE / REPZ    相等时重复(配 CMPS/SCAS)
        REPNE / REPNZ  不等时重复(配 CMPS/SCAS,找字符常用)

【方向由 DF 决定】

        CLD    DF=0,地址【递增】(从小往大,最常用)
        STD    DF=1,地址【递减】

    ★ 用串指令前记得先 CLD,尤其是刚被别的程序改过 DF 时。

【一个常见需求:把内存一块区域清零】

        MOV DI, 目标地址
        MOV CX, 100      ; 清 100 个字节
        MOV AL, 0
        CLD
        REP STOSB        ; 把 AL 存 100 次

    三行搞定,比写循环快得多。


========================================================================
  第 4 站(14-19 分钟)  控制转移
------------------------------------------------------------------------

【无条件跳转 JMP】

        JMP 标号

    三种跨度(编译器自动选):
        短跳转   EB disp8    在 ±127 字节内,2 字节
        近跳转   E9 disp16   在同一段内,3 字节
        远跳转   EA off seg  跨段,5 字节

    ★ 都用 JMP 写,汇编器自动挑最短的。

【条件跳转 Jcc:上一站(第 5 篇)讲过怎么判断】

        JZ/JE    ZF=1        JNZ/JNE  ZF=0
        JC       CF=1        JNC      CF=0
        JS       SF=1        JNS      SF=0
        JO       OF=1        JNO      OF=0
        (还有无符号的 JA/JB、有符号的 JG/JL)

    ★ 重要限制:条件跳转只能跳 ±127 字节(短跳转)。
      要跳更远,得先 JMP 到一个近处的标号再跳。

【循环指令 LOOP】

        LOOP 标号

    做的事:CX = CX − 1,然后如果 CX ≠ 0 就跳转。
    编码:E2 disp8

    变体:
        LOOPE / LOOPZ     CX≠0 且 ZF=1 时继续
        LOOPNE / LOOPNZ   CX≠0 且 ZF=0 时继续

    ★ LOOP 只能跳 ±127 字节,而且不改变标志位。

【★ CALL / RET:函数调用】

    CALL 和 JMP 的区别:CALL 会【记住回来的路】。

        CALL 标号
        · 把返回地址(下一条指令的地址)压进堆栈
        · 然后跳过去

        RET
        · 从堆栈弹出返回地址,送回 IP
        · 继续执行



    近调用(同段内):压 IP(2 字节),SP 减 2
    远调用(跨段):先压 CS 再压 IP(4 字节),SP 减 4

    ★ 这解释了两件事:
      ① 为什么 CALL 比 JMP 慢(要读写堆栈)
      ② 为什么 CALL 后 SP 会变(堆栈不平衡是汇编最常见的崩溃原因)

【中断指令 INT】

        INT n     n 是中断类型号(0~255)
        IRET      中断返回

    ★ INT 和 CALL 很像,都是"调用",区别是:
      · CALL 跳到程序里的标号
      · INT 跳到【中断向量表】里记的地址(第 8 篇细讲)
      · INT 还会把 FLAGS 也压栈,IRET 会恢复

    ★ 程序结束时常用的:
        MOV AH, 4CH
        INT 21H          ; 调用 DOS 功能,结束程序


========================================================================
  第 5 站(19-25 分钟)  汇编程序的完整结构
------------------------------------------------------------------------

    这一站教你写"能汇编能运行"的程序,而不只是片段。

【骨架】



    完整格式(MASM/TASM 风格):

        DATA SEGMENT
            STR DB 'Hello$'
        DATA ENDS

        CODE SEGMENT
            ASSUME CS:CODE, DS:DATA
        START:
            MOV AX, DATA
            MOV DS, AX
            ...
        CODE ENDS
            END START

【逐行解释】

    · SEGMENT / ENDS    划出一个段的边界(成对出现)
    · ASSUME            告诉汇编器"哪个段寄存器对应哪个段"
                        它不产生任何机器码,只是给汇编器的提示
    · 标号 START:       后面跟冒号,表示一个地址
    · MOV AX, DATA      取 DATA 段的【段地址】
    · MOV DS, AX        ★ 为什么绕一下?因为第 5 篇讲的三条铁律:
                        立即数不能直接送段寄存器
    · END START         告诉汇编器:源文件到此结束,
                        并且程序入口是 START

【常用伪指令(不产生机器码)】

        DB  1,2,3          定义字节(Define Byte)
        DW  1234H          定义字(Define Word)
        DUP 100 DUP(0)     重复 100 个 0
        EQU  PORT 60H      定义一个常量
        ORG  100H          指定起始偏移
        $                   当前地址(A - $ 可算长度)

【★ 为什么程序一开始必须设 DS】

    因为程序里的变量都在 DATA 段,而访问变量时默认用 DS。
    刚上电时 DS 是随机的(或指向 PSP),不设就会读到乱七八糟的地方。

    ★ 这是汇编程序"能编译但一运行就死"的最常见原因。

【DS 只能设一次吗】

    可以改,但改完之前的变量就访问不到了(除非改回来)。
    所以一般程序开头设一次就够。

【数据定义的实际存储(小端)】

        DW 1234H, 5678H

    在内存里是:
        地址 n     →  34H
        地址 n+1   →  12H
        地址 n+2   →  78H
        地址 n+3   →  56H

    这就是第 3 篇讲的小端模式。


========================================================================
  第 6 站(25-30 分钟)  ★ 一个能跑的完整程序
------------------------------------------------------------------------

【题目:把字符串里的小写字母全转成大写】

    输入:"Hello, World!"
    输出:"HELLO, WORLD!"

【完整代码】

        DATA SEGMENT
            STR DB 'Hello, World!$'
        DATA ENDS

        CODE SEGMENT
            ASSUME CS:CODE, DS:DATA
        START:
            MOV  AX, DATA
            MOV  DS, AX            ; 建立 DS(必不可少!)
            LEA  SI, STR           ; SI 指向字符串开头
        NEXT:
            MOV  AL, [SI]          ; 取一个字符
            CMP  AL, '$'           ; 是结束符吗?
            JE   DONE              ; 是 → 结束
            CMP  AL, 'a'           ; 小于 'a'(61H)?
            JB   SKIP              ; 是 → 不是小写,跳过
            CMP  AL, 'z'           ; 大于 'z'(7AH)?
            JA   SKIP              ; 是 → 不是小写,跳过
            SUB  AL, 20H           ; 是小写 → 减 20H 变大写
            MOV  [SI], AL          ; 写回原处
        SKIP:
            INC  SI                ; 指针后移
            JMP  NEXT              ; 下一个字符
        DONE:
            MOV  AX, 4C00H
            INT  21H               ; 结束程序,返回 DOS
        CODE ENDS
            END START

【逐条分析:为什么要这么写】

    ① LEA SI, STR       取的是【地址】,不是内容。用 LEA 比 MOV 快。
    ② MOV AL, [SI]      间接寻址(第 4 篇):SI 是"指针"。
    ③ CMP AL, '$'       判断结束。$ 是 DOS 字符串的约定结束符。
    ④ CMP AL, 'a' + JB  小于 'a' 说明不是小写字母。
                        ★ 用 JB(无符号)因为 ASCII 都是正数。
    ⑤ CMP AL, 'z' + JA  大于 'z' 也不是。
    ⑥ SUB AL, 20H       小写转大写只需减 20H。
                        ★ 验证:'a'(61H) − 20H = 41H = 'A' ✓
    ⑦ MOV [SI], AL      写回原处(间接寻址,写在方括号里)
    ⑧ INC SI            别忘了移动指针!这是初学最常漏的一步。
    ⑨ JMP NEXT          回到循环开头。注意这里用【无条件】跳转,
                        因为所有判断都已经在中间做完了。

【执行过程(前几个字符)】

    初始:SI 指向 'H'
    'H'=48H:CMP '$'→不等;CMP 'a'→48<61→JB SKIP → 跳过
    'e'=65H:CMP 'a'→65≥61 继续;CMP 'z'→65≤7A 继续 → SUB 20H → 45H='E' ✓
    'l'=6CH:同理 → 4CH='L' ✓
    'l'=6CH:→ 4CH='L' ✓
    'o'=6FH:→ 4FH='O' ✓
    ','=2CH:48<61 → JB SKIP → 跳过 ✓
    ' '=20H:→ 跳过 ✓
    'W'=57H:→ 跳过(本来就是大写)✓

    最终结果:"HELLO, WORLD!" ✓

【★ 这个程序里用到了前面所有篇的知识】

    第 1 篇:ASCII 码、'a'−'A'=20H
    第 3 篇:SI 的偏移、DS 的作用
    第 4 篇:间接寻址 [SI]、LEA
    第 5 篇:CMP、SUB、三条铁律(MOV DS,AX)
    第 6 篇:JB/JA/JE 条件跳转、JMP 循环

    ★ 如果你能独立写出这个程序,前六篇就真的学进去了。

【如果要显示出来】

    上机的话还要调用 DOS 的字符串显示功能:

        MOV AH, 09H
        LEA DX, STR
        INT 21H

    ★ 注意:显示字符串要用 DX(不是 SI),这是 DOS 的规定。
      09H 功能号从 DS:DX 处显示到 '$' 为止。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 逻辑运算和算术运算最根本的区别是什么?
  2. 清零为什么用 XOR AX,AX 而不用 MOV AX,0?
  3. TEST 和 AND 有什么区别?什么时候必须用 TEST?
  4. NOT 影响标志位吗?NEG 呢?
  5. SHL 和 SAL 有什么区别?
  6. SHR 和 SAR 有什么区别?AL=B4H 时它们分别得什么?
  7. ROL 和 RCL 有什么区别?
  8. 8086 能写 SHL AL,4 吗?不能的话怎么写?
  9. 串操作的三个默认寄存器是什么?REP 前缀做什么?
 10. CALL 和 JMP 的本质区别?CALL 后 SP 怎么变?
 11. 汇编程序里 MOV DS, DATA 为什么是非法的?正确写法?
 12. 小写转大写为什么要减 20H?'a' 减完是多少?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 算术运算有进位(位与位之间有影响),逻辑运算逐位独立、互不影响。
  2. XOR 是 2 个字节,MOV AX,0 是 3 个字节,而且 XOR 不访问存储器,更快。
  3. TEST 做 AND 但【不保存结果】,只影响标志位。
     当需要"判断某位但不想破坏原值"时必须用 TEST。
  4. NOT 不影响任何标志位;NEG 会影响(CF、SF、ZF、OF 等)。
  5. 没有区别,是同一条指令的两种写法,编码完全相同。
  6. SHR 高位补 0(当无符号),SAR 高位补符号位(保持负号)。
     B4H:SHR → 5AH;SAR → DAH(= −38)。
  7. ROL 直接把最高位补回最低位;RCL 要经过 CF 中转
     (CF 的旧值补进最低位)。RCL 用于多字节移位。
  8. 不能(8086 只支持移 1 位或移 CL 位)。
     要写:MOV CL,4 然后 SHL AL,CL。
  9. 源 DS:SI、目的 ES:DI、次数 CX。
     REP 表示重复执行 CX 次,每次自动调整 SI/DI 并 CX 减 1。
 10. JMP 直接跳,不记路;CALL 会把返回地址压进堆栈再跳。
     CALL 后 SP 减 2(近调用)或减 4(远调用)。
 11. 因为立即数不能直接送段寄存器(第 5 篇的铁律②)。
     正确写法:MOV AX, DATA 然后 MOV DS, AX。
 12. 因为 ASCII 里 'a'(61H) 和 'A'(41H) 正好差 20H(第 5 位)。
     'a' − 20H = 41H = 'A'。


========================================================================
  本篇最容易犯的 7 个错
========================================================================

  1. 用 SHR 处理负数
     应该用 SAR。SHR 会把 −76 变成 90,完全错。

  2. 以为 8086 能 SHL AL,4
     不能,必须 MOV CL,4 再 SHL AL,CL。

  3. 用 AND 判断某位,破坏了原值
     应该用 TEST。

  4. 串操作忘了 CLD
     DF 不确定时地址方向可能反了,数据全乱。

  5. CALL 后忘了平衡堆栈
     每个 CALL 必须对应一个 RET,每个 PUSH 必须对应一个 POP。

  6. 汇编程序开头忘了设 DS
     能编译通过,一运行就读到乱七八糟的数据。

  7. 循环里忘了 INC SI(移动指针)
     死循环,或者反复处理同一个字符。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 7 篇(存储器与译码):地址计算用 LEA 和寻址方式,
      这一篇的 CALL/RET 决定了程序怎么组织。
    · 第 8 篇(I/O 与中断):INT 指令在这里只是提了一下,
      那里会讲清中断向量表、中断服务程序怎么写。
    · 第 9 篇(8259A):中断服务程序就是"被 INT 调用的函数"。
    · 第 10-12 篇(接口芯片):初始化芯片全靠 OUT 指令写控制字,
      读写数据靠 IN,判断状态靠 TEST。这一篇的位操作是基础。

    一句话:这一篇是"从指令到程序"的桥梁。
    看懂第 6 站那个完整程序,你就具备了写汇编的能力。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    位图与程序结构图已设置避免跨页断开,正文按 10pt 排版。
07 存储器系统与地址译码
========================================================================
      微机原理与接口技术 · 第 7 篇
      存储器系统与地址译码 —— 30 分钟速通
========================================================================

前置:第 3 篇(地址、小端)、第 4 篇(EA)。
这一篇讲硬件:CPU 只给出一根 20 位的地址,怎么让它只选中某一颗芯片?


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

先想一个实际问题:

    你的板子上有四颗存储芯片,CPU 送出地址 12345H。
    这个地址该由哪一颗芯片响应?

    如果不加控制,四颗芯片【同时】响应 → 数据线打架 → 数据全乱。

所以必须有个机制:根据地址的高位,决定让哪一颗芯片"说话"。
这个机制就叫【地址译码】。

一共六站:

   第 1 站  存储芯片长什么样                        (4 分钟)
   第 2 站  位扩展与字扩展                          (5 分钟)
   第 3 站  ★ 地址译码与 74LS138                    (6 分钟)
   第 4 站  ★ 三种译码方式与地址范围计算            (6 分钟)
   第 5 站  8086 的奇偶分体(BHE / A0)             (4 分钟)
   第 6 站  综合练习                                (5 分钟)

★★ 本篇的核心数字(都实际算过)★★

    8K = 8192 = 2^13      → 需要 A12~A0 共 13 根地址线
    32K×8 用 4 片 8K×8     → 高位 A15~A13 送 138 译码
    四片地址范围:
        片 0  0000H ~ 1FFFH     片 1  2000H ~ 3FFFH
        片 2  4000H ~ 5FFFH     片 3  6000H ~ 7FFFH
    合起来 0000H ~ 7FFFH,共 32KB
    74LS138 输出【低电平有效】


========================================================================
  第 1 站(0-4 分钟)  存储芯片长什么样
------------------------------------------------------------------------

【四类引脚,各干一件事】



        地址线 A0~An    选"哪一个格子"
        数据线 D0~Dm    传"格子里的内容"
        片选 CS         决定"这颗芯片要不要理你"(低电平有效)
        读写 WE / OE    决定"读还是写"

    ★ 片选(CS,Chip Select)是这一篇的主角。
      只有当 CS 为低电平时,这颗芯片才会响应地址和数据线。

【容量怎么读】

        型号标注:8K×8
        含义:8192 个格子,每格 8 位

        地址线根数 = log2(8192) = 13  (A12~A0)
        数据线根数 = 8                (D7~D0)
        总容量     = 8192 × 8 bit = 8KB

    几个常见型号(背下来省事):

        2114    1K×4    SRAM    10 根地址线、4 根数据线
        6116    2K×8    SRAM    11 根地址线
        6264    8K×8    SRAM    13 根地址线
        62256   32K×8   SRAM    15 根地址线
        2764    8K×8    EPROM   13 根地址线
        27128   16K×8   EPROM   14 根地址线

    ★ 规律:型号后面的大数字 ≈ 容量(单位 Kbit),
      比如 6264 = 64Kbit = 8K×8。

【RAM 和 ROM 的区别】

        RAM(随机存取存储器)
            · 能读能写
            · 断电就丢(易失性)
            · SRAM:用触发器存,快、贵、不用刷新
            · DRAM:用电容存,慢、便宜、需要定时刷新

        ROM(只读存储器)
            · 正常只能读
            · 断电不丢(非易失性)
            · 用来存 BIOS、固件

    ★ 一句话:RAM 是"草稿纸",ROM 是"刻好的说明书"。


========================================================================
  第 2 站(4-9 分钟)  位扩展与字扩展
------------------------------------------------------------------------

    现实中的芯片容量总是不合适:想要 32K×8,手上只有 8K×8。
    怎么办?拼。



【① 位扩展:数据线不够,并排拼】

    用 1K×4 拼成 1K×8:

        需要 8 ÷ 4 = 2 片
        地址线【并联】:两片都接 A9~A0
        数据线【分开】:片 1 接 D7~D4,片 2 接 D3~D0
        片选【并联】:两片同时选中

    ★ 这样一次读写就同时操作了两片,凑出 8 位。
      位扩展的特点是:地址范围不变,位数变多。

【② 字扩展:地址空间不够,用片选用片】

    用 8K×8 拼成 32K×8:

        需要 32 ÷ 8 = 4 片
        地址线【并联】:四片都接 A12~A0(片内地址)
        数据线【并联】:四片都接 D7~D0
        片选【分开】:每片一个独立的片选信号

    ★ 关键:靠片选决定"这一次谁说话"。
      四片轮流响应,所以地址空间变成 4 倍。

【③ 字位同时扩展】

    要 32K×8,手上只有 1K×4:

        字数要 32K / 1K = 32 倍 → 需要 32 组
        位数要 8 / 4 = 2 倍   → 每组 2 片
        总共 32 × 2 = 64 片

    ★ 先算"组数"(字方向),再算"每组几片"(位方向)。

【★ 一个重要规律】

        地址线总根数 = log2(总字数)
        数据线根数   = 总位数

    32K×8:log2(32768) = 15 根地址线(A14~A0),8 根数据线。

    这 15 根分成两部分:
        A12~A0(13 根)→ 送到【每一片】的内部地址
        A15~A13(2 根)→ 用来【选片】

    ★ 这就是"译码"要干的事:把高位地址线变成片选信号。


========================================================================
  第 3 站(9-15 分钟)  ★ 地址译码与 74LS138
------------------------------------------------------------------------

【为什么需要译码器】

    要选 4 片芯片,需要 2 位地址(2² = 4)。
    但"2 位地址 → 4 个片选"这件事,不能直接用地址线接:

        如果 A14 接片 1 的 CS、A13 接片 2 的 CS
        → 当 A14=0、A13=0 时,两片都不选(都无效)
        → 地址空间出现空洞

    所以需要一个专门的芯片来做这件事:译码器。

【74LS138:3 进 8 出的"分拣员"】

    输入:C、B、A(3 位)+ 使能端 G1、G2A、G2B
    输出:Y0 ~ Y7(8 个,低电平有效)

    真值表(G1=1、G2A=G2B=0 时才工作):

        CBA = 000  →  Y0 变低,其余保持高
        CBA = 001  →  Y1 变低
        CBA = 010  →  Y2 变低
        …
        CBA = 111  →  Y7 变低

    ★ 输出是【低电平有效】:被选中的那一路变 0。
      这正好和存储器的 CS(也是低有效)直接对接,不用反相。



【32K×8 的完整连接】

        ① 4 片 6264(8K×8)
        ② 每片的 A12~A0 并联,接系统的 A12~A0
        ③ 每片的 D7~D0 并联,接系统的 D7~D0
        ④ 138 的输入:C=A15、B=A14、A=A13
        ⑤ 138 的输出 Y0~Y3 分别接四片的 CS

    于是地址分配:

        A15A14A13 = 000  →  Y0 有效  →  片 0  →  0000H ~ 1FFFH
        A15A14A13 = 001  →  Y1 有效  →  片 1  →  2000H ~ 3FFFH
        A15A14A13 = 010  →  Y2 有效  →  片 2  →  4000H ~ 5FFFH
        A15A14A13 = 011  →  Y3 有效  →  片 3  →  6000H ~ 7FFFH

    ★ 为什么片 0 的末尾是 1FFFH?因为 8K = 2000H,
      从 0000H 开始的 8K 就是 0000H ~ 1FFFH(共 2000H 个字节)。

【使能端 G1/G2 有什么用】

    用来"总开关":只有当 G1=1 且 G2=0 时,138 才输出。
    可以把其他高位地址线(比如 A19~A16)接到 G2 上,
    这样只有当那些位都满足条件时,138 才工作 —— 这就是"全译码"。

    ★ 没有用到的高位地址线一定要处理(接译码器的使能端,
      或者明确为 0),否则会出现地址重叠(下一站讲)。


========================================================================
  第 4 站(15-21 分钟)  ★ 三种译码方式
------------------------------------------------------------------------



【① 线选法】

    做法:直接用某一根高位地址线接 CS。

        例:A15 接片 1 的 CS、A14 接片 2 的 CS

    问题:
        · 片 1 只在 A15=0 时选中 → 地址在 0000H~7FFFH 之间(如果片内 8K)
          但 A14 没管,所以 A14=0 和 A14=1 都能访问到片 1 → 重叠
        · 空间不连续,中间大量空洞

    优点:最简单,不要译码器,适合芯片很少的小系统。

【② 部分译码】

    做法:只把一部分高位地址线接译码器,剩下的不管。

        例:138 只接 A15、A14、A13(这是上一站的做法),
            A19~A16 完全不接(悬空或当成无关)

    后果:地址重叠。

        因为 A19~A16 四位有 16 种组合,芯片都会响应,
        所以这 32K 的存储器会出现在:

            0000H ~ 7FFFH
            10000H ~ 17FFFH
            20000H ~ 27FFFH
            ……
            F0000H ~ F7FFFH

        共 16 段,每段间隔 10000H(64KB)。

    ★ 这就是为什么老机器上"1MB 内存只认 640KB"的混乱有一部分
      来自地址重叠和解码不完整。

【③ 全译码】

    做法:所有没参与片内寻址的高位地址线都参与译码。

        例:把 A19~A16 接到 138 的使能端(要求全为 0)

    结果:地址唯一、不重叠。

        0000H ~ 7FFFH 是这块存储器,再往上就是别的设备了。

    优点:地址干净,不会误访问。
    缺点:电路复杂一点。

【★ 地址范围怎么算(考试必考)】

    三步:

        ① 看哪几根地址线参与了译码(被固定住了)
        ② 把片内地址线全部取 0 → 得到起始地址
        ③ 把片内地址线全部取 1 → 得到结束地址

    例:A19~A13 参与译码,其中 A19~A16 = 0、A15A14A13 = 000,
       片内 A12~A0:

        起始:0000 0000 0000 0000 0000B = 00000H
        结束:0000 0111 1111 1111 1111B = 07FFFH

        所以范围是 00000H ~ 07FFFH(32KB)✓

    再例:A15A14A13 = 010(片 2):

        起始:0100 0000 0000 0000B(A15~A0)= 4000H
        结束:0101 1111 1111 1111B = 5FFFH

        ★ 但别忘了 A19~A16 也要是 0,所以完整地址还是
          004000H ~ 005FFFH


========================================================================
  第 5 站(21-25 分钟)  8086 的奇偶分体
------------------------------------------------------------------------

    这一站解释一个很实际的问题:8086 有 16 位数据线,
    但存储器是按字节编址的,怎么配合?

【答案:把存储器分成两个"体"】



        偶体:存偶地址的字节,接 D7~D0,用 A0=0 选
        奇体:存奇地址的字节,接 D15~D8,用 BHE=0 选

    BHE(Bus High Enable,总线高字节允许)是 8086 的一个引脚。

【四种组合】

        BHE  A0  |  实际传送
        ---------+---------------------------
         0    0  |  一个字(16 位,两个体同时选中)
         1    0  |  只传偶地址的字节(读 D7~D0)
         0    1  |  只传奇地址的字节(读 D15~D8)
         1    1  |  不传送(无效组合)

【★ 这就解释了第 3 篇的"规则字"】

    规则字(起始地址是偶数):
        两个字节正好落在偶体和奇体的同一行 → 一次总线周期读完

    非规则字(起始地址是奇数):
        低字节在奇体的某个地址,高字节在偶体的下一行
        → 要读两次

    验证:
        地址 10000H:A0=0 → 偶体(低字节)
        地址 10001H:A0=1 → 奇体(高字节)
        地址 10002H:A0=0 → 偶体(下一个低字节)

        所以字 10000H 的两个字节在 10000H(偶)和 10001H(奇),
        同一行,一次读完 ✓

        而字 10001H 的两个字节在 10001H(奇)和 10002H(偶)——
        跨了行,要读两次 ✓

    ★ C 语言里 struct 的"内存对齐"要求,根源就在这里:
      不对齐会让访问速度打对折。

【8088 的差别】

    8088 只有 8 位数据线(外部),所以不用分体,
    每次只传一个字节 —— 它没有 BHE 这个引脚。


========================================================================
  第 6 站(25-30 分钟)  综合练习
------------------------------------------------------------------------

【练习一:算容量和地址线】

    一颗 27128 芯片,容量是多少?要几根地址线?几根数据线?

        27128 = 128Kbit = 16K × 8
        地址线 = log2(16384) = 14 根(A13~A0)
        数据线 = 8 根

【练习二:字扩展】

    用 2K×8 的芯片构成 8K×8 的存储器,需要几片?地址怎么分配?

        8K ÷ 2K = 4 片
        片内:A10~A0(11 根,2^11 = 2K)
        选片:A12、A11

        片 0(A12A11=00):0000H ~ 07FFH
        片 1(A12A11=01):0800H ~ 0FFFH
        片 2(A12A11=10):1000H ~ 17FFH
        片 3(A12A11=11):1800H ~ 1FFFH

        合起来 0000H ~ 1FFFH = 8KB ✓

【练习三:从电路反推地址】

    已知:138 的输入 C=A15、B=A14、A=A13,使能端接了
    A19~A16 = 0 的译码结果。现在 Y5 有效(接某片 8K×8 的 CS)。
    这片芯片的地址范围是?

        Y5 对应 CBA = 101 → A15=1、A14=0、A13=1
        A19~A16 = 0

        起始:0000 1010 0000 0000 0000B
              A19..A16=0000, A15..A13=101, A12..A0=全 0
              = 0A000H

        结束:A12~A0 全 1
              = 0BFFFH

        所以范围是 0A000H ~ 0BFFFH(8KB)✓

    ★ 验算:0BFFFH − 0A000H + 1 = 1FFFH + 1 = 2000H = 8192 = 8K ✓

【练习四:为什么要片选】

    如果四颗芯片的 CS 都接地(永远有效),会发生什么?

        四颗芯片同时把数据送到数据总线上 →
        数据线冲突(有的想输出 0,有的想输出 1)→
        读到的是"线与"的随机结果,甚至可能烧坏芯片。

    ★ 所以片选绝不能不接。这是硬件设计的基本常识。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 一颗 8K×8 的芯片有几根地址线、几根数据线?总容量多少?
  2. 位扩展和字扩展各解决什么问题?
  3. 用 1K×4 的芯片做 4K×8 的存储器,需要几片?怎么分组?
  4. 片选(CS)是干什么的?有效电平是高还是低?
  5. 74LS138 有几个输入、几个输出?输出有效电平时是高还是低?
  6. 用 4 片 8K×8 组成 32K×8,138 的输入该接哪几根地址线?
  7. 承上题,四片各自的地址范围是多少?
  8. 线选法有什么缺点?
  9. 部分译码会导致什么现象?后果是什么?
 10. 全译码和部分译码的区别在哪?
 11. 8086 的 BHE 和 A0 各控制什么?BHE=1、A0=0 时传送什么?
 12. 为什么规则字比非规则字快?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 13 根地址线(A12~A0)、8 根数据线(D7~D0),总容量 8KB。
  2. 位扩展补数据线(位数不够),字扩展补地址空间(字数不够)。
  3. 4K/1K = 4 组,每组 8/4 = 2 片,共 8 片。
  4. 决定这颗芯片要不要响应地址和数据线(相当于"总开关")。低电平有效。
  5. 3 个输入(C、B、A)、8 个输出(Y0~Y7)。被选中的那一路为低电平。
  6. C=A15、B=A14、A=A13。
  7. 0000H~1FFFH、2000H~3FFFH、4000H~5FFFH、6000H~7FFFH。
  8. 地址不连续、大量重叠、浪费空间。但电路最简单。
  9. 地址重叠:同一块存储器会出现在多个地址上
     (每隔 64KB 重复一次)。后果是可能误访问、掩盖硬件错误。
 10. 全译码让所有未参与片内的地址线都参与译码,地址唯一不重叠;
     部分译码把一些高位线当"无关",导致地址重复出现。
 11. BHE 控制奇体(D15~D8)是否选中,A0 控制偶体(D7~D0)是否选中。
     BHE=1、A0=0 时只传偶地址的字节(读 D7~D0)。
 12. 因为规则字的两个字节正好落在偶体和奇体的同一行,
     一次总线周期就能读完;非规则字跨行,要读两次。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 算地址线根数时算错
     1K = 1024 = 2^10 → 10 根。8K = 2^13 → 13 根。别把 K 当 1000。

  2. 忘了片选不能悬空
     不接片选的芯片会一直响应,导致数据总线冲突。

  3. 地址范围内算错(多算或少算 1)
     8K 从 0000H 开始,到 1FFFH 结束(不是 2000H)。
     验证:1FFFH − 0000H + 1 = 2000H = 8192 ✓

  4. 138 的输出极性记反
     输出是【低电平有效】,和 CS 的低有效正好匹配,不用加反相器。

  5. 忘记处理没用到的地址线
     A19~A16 不接就会地址重叠。要么接使能,要么明确为 0。

  6. 认为 8086 一次总能读一个字
     非规则字要读两次。这就是内存对齐存在的原因。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 8 篇(I/O 与中断):I/O 端口也需要译码!
      只不过用的是 16 位 I/O 地址,而且是单独的空间(M/IO 引脚区分)。
    · 第 9-12 篇:接口芯片也有片选,接法和存储器一样。
      8255A、8253、8259A 的 CS 都要接译码器输出。
    · 第 13 篇(总线与现代微机):现代内存条用 SPD、分页,
      原理上仍是"高位选芯片、低位选单元"。

    一句话:这一篇是"硬件视角的存储器"。
    重点记三样:容量怎么算、138 怎么用、地址范围怎么推。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    电路图与表格已设置避免跨页断开,正文按 10pt 排版。
08 输入输出与中断
========================================================================
      微机原理与接口技术 · 第 8 篇
      输入输出与中断 —— 30 分钟速通
========================================================================

前置:第 3 篇(段)、第 6 篇(INT 指令)。
从这一篇开始进入【接口技术】—— 前面讲 CPU 自己,后面讲它怎么跟外界打交道。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

CPU 只会算数和搬数据。可它要控制打印机、读键盘、显示屏幕 ——
这些都是"外设"。

问题:CPU 和外设根本不是一个世界的东西。
    CPU 快得离谱(纳秒),打印机慢得可怜(毫秒)
    电压电平不一样
    数据格式不一样(并行 / 串行)
    时序完全不同步

所以中间必须有个"翻译 + 缓冲"的东西 —— 这就是【I/O 接口】。

接口要解决的核心矛盾是:CPU 怎么知道外设准备好了?

一共有三种答案,就是三种 I/O 方式:

    查询:CPU 自己去问(干等)
    中断:外设主动举手(CPU 不用等)
    DMA :搬用工自己干(CPU 完全不参与)

其中【中断】是最重要、也是考试考得最多的。

一共六站:

   第 1 站  为什么需要 I/O 接口                      (4 分钟)
   第 2 站  I/O 端口与 IN / OUT 指令                 (5 分钟)
   第 3 站  三种 I/O 方式                            (5 分钟)
   第 4 站  中断的基本概念与分类                      (5 分钟)
   第 5 站  ★ 中断响应的完整过程                    (6 分钟)
   第 6 站  中断服务程序怎么写 + 自测                (5 分钟)

★★ 本篇的核心数字(都实算过)★★

    IN  AL, 60H      E4 60H        IN  AL, DX      ECH
    OUT 60H, AL      E6 60H        OUT DX, AL      EEH
    中断向量表 256 × 4 = 1KB,在 00000H ~ 003FFH
    中断 08H 的入口存在 0020H(08H × 4)
    中断响应压 6 个字节:FLAGS → CS → IP,SP 减 6
    IRET 逆序弹出:IP → CS → FLAGS


========================================================================
  第 1 站(0-4 分钟)  为什么需要 I/O 接口
------------------------------------------------------------------------



    CPU 和外设之间有【四道鸿沟】:

        ① 速度不匹配    CPU 纳秒级,打印机毫秒级(差一百万倍)
        ② 电平不匹配    CPU 是 TTL 电平(0/5V),电机要 220V
        ③ 数据格式不同  CPU 并行 8/16 位,有些外设是串行的
        ④ 时序不同步    外设有它自己的节奏,不会跟着 CPU 的时钟走

    I/O 接口要做的就是"填平这四道鸿沟":

        缓冲    外设慢,就先存到接口里,CPU 不用等
        匹配    做电平转换(比如 5V ↔ 12V)
        转换    并行↔串行、数字↔模拟(第 12 篇的 A/D、D/A)
        握手    "你准备好了吗?""好了,来吧"

    ★ 一句话:接口 = CPU 和外设之间的翻译兼秘书。

【接口里有什么】

    一个典型的接口芯片(比如第 10 篇的 8255A)内部有:

        数据端口    暂存要传的数据(相当于快递柜)
        状态端口    告诉 CPU "我准备好没"(相当于指示灯)
        控制端口    接收 CPU 的命令(相当于操作面板)

    ★ 这三个端口各有自己的【端口地址】,CPU 用 IN/OUT 访问。
      这是后面所有接口芯片的统一套路。


========================================================================
  第 2 站(4-9 分钟)  I/O 端口与 IN / OUT 指令
------------------------------------------------------------------------

【两套独立的地址空间】



    8086 有两套地址,是【分开的】:

        存储器空间   20 位地址 → 1MB    用 MOV / LEA 访问
        I/O 空间     16 位地址 → 64KB   用 IN / OUT 访问

    靠一个引脚区分:M/IO(M 上面有横线,表示低有效)

        M/IO = 1  →  访存
        M/IO = 0  →  访端口

    ★ 重要:地址 0060H 在存储器里和 I/O 里是【两个完全不同的地方】。
      所以 MOV AL,[60H] 和 IN AL,60H 干的是两件毫不相干的事。

【IN / OUT 指令】

        IN  AL, 端口号      从端口读一个字节到 AL
        OUT 端口号, AL      把 AL 写到一个端口

    两条必须记住的:

        ★ 数据必须经过 AL / AX。不能 IN BX, 60H(非法)。
        ★ 端口号 ≤ 255 时可以直接写;> 255 必须用 DX。

【四种形式与机器码】

        IN  AL, 60H     E4 60H     端口号在指令里(8 位)
        IN  AX, 60H     E5 60H     一次读两个字节
        OUT 60H, AL     E6 60H
        OUT 60H, AX     E7 60H

        IN  AL, DX      ECH        端口号在 DX 里(16 位)
        IN  AX, DX      EDH
        OUT DX, AL      EEH
        OUT DX, AX      EFH

    规律:E4~E7 是"端口号写在指令里",EC~EF 是"端口号在 DX 里"。
          低 2 位表示:00=IN AL、01=IN AX、10=OUT AL、11=OUT AX。

    ★ 一个常问的问题:既然 DX 间接只要 1 字节,为什么还用直接寻址?
      因为直接寻址总共只要 2 字节(E4 60),而 DX 间接要
      "MOV DX,port(3 字节)+ IN AL,DX(1 字节)" = 4 字节。
      所以端口在 0~255 时,直接寻址更省。

【为什么 I/O 空间只有 64KB】

    因为 8086 的 I/O 地址只有 16 位。
    实际根本用不到 64KB —— PC 上只用了 0000H~03FFH 这一小段。

    ★ 常用端口(记几个有印象):
        60H   键盘数据口
        61H   扬声器 / 键盘控制
        20H   8259A 主片(第 9 篇)
        21H   8259A 主片的数据口
        3F8H  串口 COM1(第 11 篇)


========================================================================
  第 3 站(9-14 分钟)  三种 I/O 方式
------------------------------------------------------------------------



【① 程序查询(轮询)】

    思路:CPU 不停地去问"好了没"。

        AGAIN: IN   AL, 状态端口
               TEST AL, 01H       ; 看"准备好"位
               JZ   AGAIN         ; 没好就再问
               IN   AL, 数据端口  ; 好了,传数据

    优点:最简单,不用中断系统。
    缺点:CPU 全耗在等待上。如果外设 1 毫秒才准备好,
           CPU 这 1 毫秒什么也干不了。

    ★ 这叫"忙等待",效率最低。但小系统里常用,因为简单。

【② 中断】

    思路:外设准备好了,主动发信号"打断"CPU。

        外设 → 发 INTR → CPU 停下手上的活,去处理 → 处理完回来接着干

    优点:CPU 不用空等,等待期间可以干别的。
    缺点:每次中断都要压栈、弹栈、查表,有开销。

    ★ 这是本篇的重点,下一站详细讲。

【③ DMA(直接存储器访问)】

    思路:找一个"搬用工"(DMA 控制器),让它直接在存储器和外设之间搬数据,
          CPU 完全不参与。

        CPU 只做三件事:
            ① 告诉 DMA 控制器:从哪搬到哪、搬多少
            ② 启动 DMA
            ③ 搬完了通知我一声

    优点:速度最快,而且不占 CPU。
    缺点:DMA 要"抢"总线(CPU 要暂时让出总线),电路复杂。

    ★ 用在哪:磁盘读写、网卡收包、显存刷新、声卡 ——
      凡是"大批量、高速"的传输,都用 DMA。

【三者怎么选】

        慢速、少量、偶尔       →  查询
        随机、实时、较频繁     →  中断
        大批量、高速           →  DMA

    ★ 实际系统里三种都有:键盘用中断,磁盘用 DMA,某些传感器用查询。


========================================================================
  第 4 站(14-19 分钟)  中断的基本概念
------------------------------------------------------------------------

【什么是中断】

    中断 = CPU 在执行程序的过程中,被"临时打断",
           转去执行一段专门的处理程序,处理完再回来接着执行。

    ★ 类比:你在看书(主程序),电话响了(中断请求),
      你记下页码(压栈),接完电话(中断服务),
      翻回刚才那页继续看(IRET)。

【几个术语】

        中断源      谁引起的中断(外设、定时器、内部异常)
        中断请求    外设发出的信号(INTR 引脚变高)
        中断响应    CPU 决定"好,我处理"
        中断服务程序 处理中断的那段代码(ISR)
        中断返回    IRET 指令,回到被打断的地方

【★ 中断类型号:中断的"身份证号"】

    每个中断都有一个编号(0~255),叫【中断类型号】。
    CPU 就是靠这个号去【中断向量表】里查入口地址的。

    内部中断的类型号是固定的:
        0 号    除法错(DIV 溢出或除 0)
        1 号    单步(TF=1 时每条指令后)
        3 号    断点(INT 3,单字节指令 CCH)
        4 号    溢出(INTO,OF=1 时)
        n 号    INT n 指令(用户自定义)

    外部中断:
        NMI     不可屏蔽中断,类型号固定为 2
        INTR    可屏蔽中断,类型号由 8259A 提供(第 9 篇)

【两类中断的区别】

        内部中断(软件中断)
            · 由指令或异常引起
            · 不受 IF 标志影响(一定响应)
            · INT n / INTO / 除法错 / 单步

        外部中断(硬件中断)
            · 由外部信号引起
            · 受 IF 标志控制(IF=0 时不响应 INTR)
            · NMI 不受 IF 影响(所以叫"不可屏蔽")

【★ 优先级(高到低)】

        除法错 > INT n > INTO > NMI > INTR > 单步

    ★ 为什么内部中断优先级高?因为它们是"已经发生的事实",
      比如除法溢出,你不能不管它接着往下算。

【为什么要"屏蔽"中断】

    IF(中断允许标志)就是总开关:
        STI   IF=1,允许中断
        CLI   IF=0,禁止中断

    ★ 用在哪:修改关键数据时(比如更新中断向量表),
      必须先用 CLI 关中断,改完再 STI 打开。
      否则改到一半来了中断,就会用到"半成品"的向量。


========================================================================
  第 5 站(19-25 分钟)  ★ 中断响应的完整过程
------------------------------------------------------------------------

    这一站是硬件中断的全流程,考试必考。



【六步走完】

    ① 外设举手中断请求
       外设把 INTR 引脚拉高(表示"我有事")

    ② CPU 做完了当前这条指令
       ★ 关键:中断【不会打断一条指令】,一定要等它执行完。
         因为指令执行到一半,寄存器和标志位都是"半成品",
         这时候去响应中断,保存的现场就是错的。

    ③ CPU 响应:发两个 INTA 负脉冲
       第一个 INTA:告诉外设"我知道了"
       第二个 INTA:外设趁机把【中断类型号 n】送上数据线

       ★ 为什么是两个脉冲?因为 8086 要先把总线空出来,
         再接收类型号,中间需要一点时间。

    ④ 压栈保护现场(6 个字节)
       顺序:先压 FLAGS,再压 CS,最后压 IP
       每压 2 个字节 SP 减 2,所以总共 SP 减 6

       ★ 同时 CPU 会自动把 IF 清 0(关中断),
         这样进入服务程序后不会再被同级中断打断
         (除非程序自己 STI 开中断,这叫中断嵌套)。

    ⑤ 查中断向量表
       从 n×4 处取 2 字节 → IP
       从 n×4+2 处取 2 字节 → CS

    ⑥ 跳到中断服务程序
       执行完后用 IRET 返回

【★ 为什么先压 FLAGS】

    因为要保存"中断发生那一刻的状态"。
    FLAGS 里有 IF、TF 等标志,如果先压 CS/IP 再压 FLAGS,
    中间万一被改动就错了。

    正确顺序:FLAGS → CS → IP
    所以栈顶是 IP(最后压的),IRET 正好先弹 IP。



【实算:中断 08H 的入口】

    假设向量表里:

        [0020H] = 00H, [0021H] = 10H   →  IP = 1000H
        [0022H] = 00H, [0023H] = F0H   →  CS = F000H

    入口物理地址 = F0000H + 1000H = F1000H ✓

    ★ 注意是小端:低字节在低地址。

【实算:压栈过程】

    中断前 SP = 0100H:

        压 FLAGS  →  SP = 00FEH,存在 SS:00FEH
        压 CS     →  SP = 00FCH,存在 SS:00FCH
        压 IP     →  SP = 00FAH,存在 SS:00FAH

    所以中断服务程序一开始,SS:SP = SS:00FAH 指向 IP。

    IRET 的顺序正好相反:
        弹 IP(SP+2) → 弹 CS(SP+2) → 弹 FLAGS(SP+2)
        最后 SP 回到 0100H ✓

【软中断 INT n 有什么不同】

        INT n 指令做的事和硬件中断几乎一样:
            压 FLAGS → 压 CS → 压 IP → 查表 → 跳转

        区别只有:
            · 类型号来自指令,不是来自外设
            · 不需要 INTA 脉冲
            · 不受 IF 影响(内部中断)

    ★ 所以 INT 21H 和硬件中断最终都走"查向量表"这条路,
      这就是为什么它们能共用同一套服务程序。

    ★ 常用:INT 21H 是 DOS 功能调用(程序结束时用 4CH 号功能)。
            4 个字节的向量表项指向 DOS 的代码。


========================================================================
  第 6 站(25-30 分钟)  中断服务程序怎么写
------------------------------------------------------------------------

【四条套路(必须按顺序)】

        ① 保护现场    PUSH 用到的寄存器
        ② 开中断      STI(允许中断嵌套;不希望嵌套就不写)
        ③ 干活        (硬件中断还要在中途或最后发 EOI,第 9 篇讲)
        ④ 恢复现场    POP(逆序!)+ IRET

【标准骨架】

        MY_ISR:
            PUSH AX            ; ① 保护现场
            PUSH DX
            PUSH DS            ; 如果要用自己的数据段,还要保存 DS
            MOV  AX, MY_DATA
            MOV  DS, AX        ; (中断程序里一般要自己设 DS)

            STI                ; ② 开中断(允许嵌套)

            IN   AL, 数据端口  ; ③ 干活
            ...
            MOV  AL, 20H
            OUT  20H, AL       ;    发 EOI,通知 8259A

            POP  DS            ; ④ 恢复现场(注意是逆序!)
            POP  DX
            POP  AX
            IRET               ; 返回

【★ 最容易出错的三件事】

    ① PUSH 和 POP 不配对
       压了 3 个只弹 2 个 → 栈不平衡 → IRET 弹到错误地址 → 程序飞掉。
       ★ 这是中断程序崩溃的第一大原因。

    ② POP 顺序搞反
       PUSH 是 AX、DX、DS,POP 就必须是 DS、DX、AX(后进先出)。

    ③ 忘了 IRET 用了 RET
       RET 只弹 IP(近返回),不会弹 CS 和 FLAGS。
       在中断程序里用 RET 会导致 FLAGS 和 CS 错乱,系统直接崩。

【装中断向量:怎么把自己的程序挂上去】

    场景:你想用自己的程序替换某个中断(比如接管 INT 1CH 系统定时)。

        CLI                          ; 关中断,防止装到一半被打断
        MOV  AX, 0
        MOV  ES, AX                  ; ES = 0000H(向量表在段 0)
        MOV  DI, 1CH * 4             ; 第 1CH 号中断 → 偏移 0070H
        MOV  AX, OFFSET MY_ISR
        MOV  ES:[DI], AX             ; 写 IP
        MOV  AX, SEG MY_ISR
        MOV  ES:[DI+2], AX           ; 写 CS
        STI                          ; 开中断

    ★ 关键点:
      · 向量表在段 0,所以 ES=0
      · 偏移 = 类型号 × 4
      · 先写 IP(低 2 字节)再写 CS(高 2 字节)—— 小端

    ★ 一个好习惯:先保存原来的向量,程序退出前恢复,否则系统会挂。

【为什么中断程序要"短"】

    中断服务程序执行期间,同级的其他中断被屏蔽(IF=0)。
    如果服务程序太长,其他中断就会被严重延误,甚至丢失。

    ★ 原则:中断程序只做"最必须的事"
      (比如:读走数据、置一个标志),
      繁重的处理留给主程序去做。

    ★ 更专业的做法:中断里只把数据放进缓冲区、置标志位,
      主程序循环检查这个标志,这叫"前后台结构"。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 为什么 CPU 不能直接接外设?I/O 接口解决哪四个问题?
  2. 8086 的两套地址空间各多大?靠什么引脚区分?
  3. IN AL,60H 和 MOV AL,[60H] 有什么区别?
  4. IN AL,60H 的机器码?为什么数据必须经过 AL/AX?
  5. 端口号什么时候必须用 DX?为什么端口在 0~255 时反而用直接寻址更省?
  6. 三种 I/O 方式各适合什么场合?
  7. 中断向量表在哪个地址范围?为什么正好 1KB?
  8. 中断类型号 09H 的入口地址存放在哪两个字节?
  9. 中断响应时压栈的顺序是什么?为什么是这个顺序?
 10. 硬件中断和 INT n 指令有什么区别?
 11. 中断服务程序必须做什么?PUSH/POP 为什么要逆序?
 12. 为什么中断服务程序要尽量短?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 速度、电平、数据格式、时序四道鸿沟。接口负责缓冲、匹配、转换、握手。
  2. 存储器 1MB、I/O 64KB。靠 M/IO 引脚区分(=1 访存,=0 访端口)。
  3. IN AL,60H 访问 I/O 端口 60H;MOV AL,[60H] 访问存储器地址 0060H。
     两者是完全不同的两个地方。
  4. E4 60H。因为 8086 规定 I/O 数据只能通过 AL/AX 传送。
  5. 端口号大于 255(FFH)时必须用 DX。
     端口 ≤ 255 时,直接寻址只要 2 字节;用 DX 要 4 字节(MOV 3 + IN 1)。
  6. 查询:慢速少量;中断:随机实时;DMA:大批量高速。
  7. 00000H ~ 003FFH。256 种中断 × 4 字节(IP+CS)= 1024 字节 = 1KB。
  8. 09H × 4 = 24H,所以存放在 0024H ~ 0027H(0024H、0025H 放 IP,
     0026H、0027H 放 CS)。
  9. 顺序:FLAGS → CS → IP(共 6 字节,SP 减 6)。
     这样最后压的 IP 在栈顶,IRET 正好先弹出 IP。
 10. INT n 的类型号来自指令、不需要 INTA 脉冲、不受 IF 影响;
     硬件中断的类型号由外设提供、需要两个 INTA 脉冲、INTR 受 IF 控制。
     两者最终都查同一个中断向量表。
 11. 保护现场 → 开中断 → 处理 → 发 EOI → 恢复现场 → IRET。
     PUSH 是"后进",POP 必须"先出",所以顺序相反。
 12. 因为服务程序执行期间 IF=0,同级中断被屏蔽。
     太长会导致其他中断延误或丢失。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 以为中断能打断一条指令
     不能。必须等当前指令执行完才响应。

  2. PUSH/POP 不配对或顺序反了
     栈不平衡 → IRET 弹错地址 → 程序飞掉。这是头号崩溃原因。

  3. 中断程序里用 RET 而不是 IRET
     RET 只弹 IP,不弹 CS 和 FLAGS,系统状态全乱。

  4. 把 I/O 端口地址和存储器地址搞混
     0060H 端口 ≠ 0060H 存储单元,靠 M/IO 区分。

  5. 忘了装向量表前先 CLI
     装到一半被打断,会用到"半成品"的向量表项。

  6. 认为 NMI 也能被屏蔽
     NMI 是"不可屏蔽中断",IF=0 时它照样响应。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 9 篇(8259A):本篇讲的 INTR/INTA 时序,8259A 就是那个
      "帮外设排队举手、并给出类型号"的芯片。
    · 第 10-12 篇:所有接口芯片的端口都用 IN/OUT 访问,
      很多还靠中断通知 CPU(比如 8255A 可以配成中断方式)。
    · 第 11 篇(8253 定时器):定时器中断就是最典型的周期性中断。
    · 第 12 篇(A/D):转换完成后常以中断通知 CPU 来取数据。

    一句话:这一篇是"接口技术"的地基。
    重点记三样:两套地址空间、三种 I/O 方式、中断响应六步。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    流程图与表格已设置避免跨页断开,正文按 10pt 排版。
09 8259A中断控制器
========================================================================
      微机原理与接口技术 · 第 9 篇
      8259A 可编程中断控制器 —— 30 分钟速通
========================================================================

前置:第 8 篇(中断响应六步、向量表)。
上一篇留了个问题:好几个外设都想中断,怎么办?
这一篇就是答案。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

8086 只有一根 INTR 引脚。可是键盘、定时器、串口、硬盘都要打断 CPU。
一根线接不了五个信号。

而且就算硬接上去,还有三个问题回答不了:

        ① 谁来喊?      INTR 只能表示"有人要中断",不能表示"是谁"
        ② 谁先喊?      两个同时举手,先处理哪个
        ③ 怎么区分?    CPU 怎么知道该跳哪个中断服务程序

8259A 就是专门解决这三个问题的芯片:

        ① 8 根 IR 线,分别接 8 个外设
        ② 内部有优先级裁决器,按优先级挑一个
        ③ 被选中时,把"中断类型号"送上数据线

    ★ 一句话:8259A 是个"会排队、会通报姓名"的门房。

一共六站:

   第 1 站  为什么需要 8259A                        (4 分钟)
   第 2 站  内部结构:三个寄存器                     (5 分钟)
   第 3 站  端口与引脚                              (4 分钟)
   第 4 站  ★ ICW1~ICW4:初始化                     (7 分钟)
   第 5 站  ★ OCW1~OCW3:屏蔽与 EOI                 (6 分钟)
   第 6 站  完整的初始化与使用 + 自测                (4 分钟)

★★ 本篇的核心数字(全部实算)★★

    PC 上端口:主片 20H / 21H,从片 A0H / A1H
    ICW1 = 13H   (单片、边沿触发、要 ICW4)
    ICW2 = 08H   (主片,IR0~IR7 → 类型 08H~0FH)
    ICW2 = 70H   (从片,IR0~IR7 → 类型 70H~77H)
    ICW3 = 04H   (主片,从片接在 IR2)
    ICW4 = 01H   (8086 模式)
    OCW1 = FEH   (只允许 IR0)
    OCW2 = 20H   (非指定 EOI,最常用)


========================================================================
  第 1 站(0-4 分钟)  为什么需要 8259A
------------------------------------------------------------------------



【三个问题,三个答案】

    ① 8 根 IR 线(IR0~IR7)分别接 8 个外设
       → 解决了"是谁"的问题

    ② 优先级裁决器
       → 解决了"先处理谁"的问题
       → 默认 IR0 优先级最高,IR7 最低

    ③ 中断类型号寄存器
       → 解决了"CPU 怎么知道跳哪"的问题
       → 在第二个 INTA 时把类型号送上数据线

【级联:8 根不够怎么办】

    一根 IR 线还可以再接一片 8259A(叫"从片")。
    主片 8 根 + 从片 8 根,最多可以扩展到 64 个中断源。

    ★ PC 上就是这样的:主片的 IR2 接了从片。
      所以主片实际可用 IR0、IR1、IR3~IR7(8 个),
      从片提供另外 8 个(IRQ8~IRQ15)。

【PC 上的实际连接(了解)】

        IR0  定时器(每秒 18.2 次)
        IR1  键盘
        IR2  接从片
        IR3  串口 COM2
        IR4  串口 COM1
        IR6  软盘
        IR7  打印机(并口)

    从片(IRQ8~15):实时时钟、PS/2 鼠标、协处理器、硬盘……

    ★ 记住 IR0 是定时器、IR1 是键盘就够了,考试常考。


========================================================================
  第 2 站(4-9 分钟)  内部结构:三个寄存器
------------------------------------------------------------------------



【三个互相配合的寄存器】

    IRR(Interrupt Request Register)中断请求寄存器
        8 位,每一位对应一根 IR 线。
        外设举手 → 对应位置 1。
        ★ 它记的是"谁举手了、还没被处理"。

    ISR(In Service Register)中断服务寄存器
        8 位。
        CPU 响应了某个中断 → 对应位置 1,开始服务。
        服务程序发 EOI → 对应位清 0。
        ★ 它记的是"谁正在被服务"。

    IMR(Interrupt Mask Register)中断屏蔽寄存器
        8 位。
        由 OCW1 设置,1 表示屏蔽(挡住),0 表示允许。
        ★ 它记的是"谁被关在门外"。

【完整流程(一句话串起来)】

    IRR(谁举手)
      → 减去 IMR 挡住的
      → 裁决器挑出最高优先级
      → 置入 ISR(开始服务)
      → 发 INT 给 CPU
      → 第二个 INTA 送出类型号
      → 服务程序结束时发 EOI,ISR 对应位清 0

    ★ 如果忘了发 EOI,ISR 那一位永远是 1,
      8259A 就认为"这个中断还在处理中",
      于是同级和更低级的中断【永远不再响应】。
      这就是"系统好像死了"的一个经典原因。

【★ 优先级的两条规则】

    ① 固定优先级:IR0 最高,IR7 最低(默认)

    ② 中断嵌套:正在服务的中断(ISR 里为 1 的那一位)优先级最高,
       它不允许同级和更低级的中断打断自己。
       只有更高优先级的可以插进来。

    ★ 举例:正在处理 IR3,此时 IR1 来了。
      IR1 比 IR3 优先级高 → 可以嵌套(打断 IR3)。
      IR5 来了 → 不行,要等 IR3 处理完。

    ★ 但如果程序里 STI 开了中断,而服务程序又没及时发 EOI,
      可能出现"同一个中断反复嵌套自己" → 栈溢出。
      所以规范做法:进入服务程序后,先发 EOI 再开中断,
      或者干脆不 STI(不允许嵌套)。


========================================================================
  第 3 站(9-13 分钟)  端口与引脚
------------------------------------------------------------------------



【A0 一个引脚,分出两组端口】

        A0 = 0  →  主端口
                   写 ICW1、OCW2、OCW3
                   读 IRR / ISR(需要先写 OCW3 指定读哪个)
        A0 = 1  →  从端口
                   写 ICW2、ICW3、ICW4、OCW1

    ★ 为什么 ICW1 在 A0=0 而 ICW2~4 在 A0=1?
      因为 ICW1 有一个【特征位】(D4=1),8259A 靠它判断
      "这是初始化开始"。而 ICW2~4 是接着 ICW1 顺序写的,
      不需要特征位。
      同样,OCW2/OCW3 也在 A0=0,靠 D4/D3 的组合区分。

【PC 上的地址】

        主片:20H(A0=0)、21H(A0=1)
        从片:A0H(A0=0)、A1H(A0=1)

    ★ 记住 20H/21H 这两个地址,写代码时天天用。

【其他引脚】

        IR0~IR7     接外设的中断请求
        INT         送 8086 的 INTR
        INTA        收 8086 的中断响应脉冲
        CAS0~CAS2   级联总线(主从之间传"是哪个从片")
        CS          片选(第 7 篇的译码输出接这里)
        D7~D0       数据线
        RD/WR       读/写控制


========================================================================
  第 4 站(13-20 分钟)  ★ ICW1~ICW4:初始化
------------------------------------------------------------------------

    初始化命令字只写一次(上电或复位后)。
    顺序是固定的:ICW1 → ICW2 → ICW3 → ICW4。
    写错顺序或写错一位,整颗芯片都不工作。



【ICW1:告诉 8259A "我要开始初始化了"】

        bit7~4 = 0001(特征位,必须是 0001,这是识别的关键)
        bit3   = LTIM  0=边沿触发,1=电平触发
        bit2   = ADI   8086 模式固定为 0
        bit1   = SNGL  1=单片,0=级联(要写 ICW3)
        bit0   = IC4   1=要写 ICW4

    常用值:13H = 0001 0011B
        0001(特征位)、LTIM=0(边沿)、SNGL=1(单片)、IC4=1(要 ICW4)

    ★ 一个容易搞混的点:SNGL 这一位

      13H = 0001 0011,其中 bit1 = 1,表示【单片】。
      可 PC 的主片明明接了从片(级联),教科书上却常写 13H —— 为什么?

      因为很多教材举的是【单片系统】的例子。
      PC 的实际值(级联)是:

        主片 ICW1 = 11H(bit1 = 0,级联)
        从片 ICW1 = 11H

      做题时看清题目是"单片"还是"级联",别死记一个数。
      本篇为了讲清原理,统一用【单片 + 13H】。

【ICW2:设定中断类型号基值】

        bit7~3 = T7~T3(类型号的高 5 位)
        bit2~0 = 0(8086 模式下由 8259A 自动填 IR 号)

    写入 08H = 0000 1000B:
        高 5 位 = 00001 → 基值 08H
        于是 IR0 → 类型 08H、IR1 → 09H、…、IR7 → 0FH

    ★ 为什么低 3 位一定要是 0?
      因为 IR 号(0~7)就填在低 3 位里。
      所以基值必须是 8 的倍数(08H、10H、18H、20H……都可以)。

    从片写 70H = 0111 0000B:
        从片 IR0 → 类型 70H、IR1 → 71H、…、IR7 → 77H

【ICW3:级联连接】

    主片:D7~D0 对应 IR7~IR0,哪一位写 1 表示那个 IR 上接了从片

        从片接在 IR2 → ICW3 = 04H = 0000 0100B

    从片:D2~D0 表示自己接在主片的哪个 IR 上

        接在 IR2 → ICW3 = 02H = 0000 0010B

    ★ 注意主片和从片的 ICW3 含义不同!
      主片是"位图"(哪几位接了从片),从片是"编号"(我接在第几号)。

【ICW4:工作方式】

        bit7~5 = 0
        bit4   = SFNM(特殊全嵌套,一般 0)
        bit3~2 = BUF(缓冲方式,一般 00)
        bit1   = AEOI(自动 EOI,一般 0=手动)
        bit0   = uPM(1=8086/8088 模式,0=8080 模式)

    常用值:01H
        uPM=1 → 8086 模式

    ★ uPM 这一位极其重要:如果写成 0,8259A 会按 8080 的方式
      组织类型号,产生的中断号全错,程序跑飞。


========================================================================
  第 5 站(20-26 分钟)  ★ OCW1~OCW3:运行中的控制
------------------------------------------------------------------------

    ICW 只在初始化时写一次。运行中随时可以写的是 OCW。



【OCW1:屏蔽字(写到 A0=1)】

        D7~D0 对应 IR7~IR0,【1 = 屏蔽,0 = 允许】

    实算:

        OCW1 = FFH  → 全部屏蔽
        OCW1 = FEH  → 只允许 IR0(其余 7 个全屏蔽)
        OCW1 = FCH  → 只允许 IR0、IR1
        OCW1 = 7FH  → 只允许 IR7(IR0~IR6 全屏蔽)

    ★ 注意方向:1 是屏蔽,不是允许。
      这个和很多人的直觉相反,考试常考。

    ★ 用法:想临时关掉某个中断源,就置对应位为 1。
      比 CLI 精细(CLI 是全局关中断)。

【OCW2:EOI(写到 A0=0)】

        bit7~5 = R、SL、EOI

    三种常用:

        20H      R=0 SL=0 EOI=1    非指定 EOI(最常用)
        60H + L  R=0 SL=1 EOI=1    指定 EOI(复位第 L 位)
        E0H + L  R=1 SL=1 EOI=1    循环指定 EOI

    ★ 99% 的场合都用 20H。它的意思是"当前正在服务的那个中断处理完了",
      8259A 会自动把 ISR 里当前最高优先级的那一位清 0。

    代码:

        MOV AL, 20H
        OUT 20H, AL          ; 主片发 EOI

    ★ 如果是【从片】的中断,要发两次 EOI:
      先给从片(OUT A0H)、再给主片(OUT 20H)。
      因为主片不知道从片的事,要分别通知。

【OCW3:读状态(写到 A0=0)】

        OUT 0AH, AL  →  下次读 A0=0 端口得到 IRR(谁举手)
        OUT 0BH, AL  →  下次读 A0=0 端口得到 ISR(谁在服务)

    代码:

        MOV AL, 0AH
        OUT 20H, AL
        IN  AL, 20H          ; AL = IRR

    ★ 用途:调试时看"到底有没有中断请求"。
      如果 ISR 某一位一直是 1,说明那个中断忘了发 EOI。

【★ 忘了发 EOI 会怎样】

    这是初学最常踩的坑,后果很严重:

        ISR 那一位永远是 1 → 8259A 认为"这个中断还在处理中"
        → 同级和更低级的中断永远不再响应
        → 系统看起来像死了(键盘没反应、定时器不走)

    所以:每个硬件中断服务程序的末尾都必须发 EOI。
    而且顺序要正确:先处理完事情,再发 EOI。


========================================================================
  第 6 站(26-30 分钟)  完整实例 + 自测
------------------------------------------------------------------------

【场景:初始化 PC 主片 8259A,只允许 IR0(定时器)和 IR1(键盘)】

        ; ① 初始化(只做一次)
        MOV AL, 13H
        OUT 20H, AL          ; ICW1:边沿触发、单片、要 ICW4
        MOV AL, 08H
        OUT 21H, AL          ; ICW2:类型号基值 08H
        MOV AL, 04H
        OUT 21H, AL          ; ICW3:从片接 IR2(单片时可省)
        MOV AL, 01H
        OUT 21H, AL          ; ICW4:8086 模式

        ; ② 设置屏蔽:只允许 IR0、IR1
        MOV AL, 0FCH
        OUT 21H, AL          ; OCW1:FEH 是只允许 IR0,FCH 是 IR0+IR1

        ; ③ 开中断(CPU 这边)
        STI

【中断服务程序(IR1 键盘)】

    KEY_ISR:
        PUSH AX
        PUSH DS
        MOV  AX, DATA
        MOV  DS, AX

        IN   AL, 60H         ; 读键盘扫描码
        ...                  ; 处理

        MOV  AL, 20H         ; ★ 发 EOI,不能忘!
        OUT  20H, AL

        POP  DS
        POP  AX
        IRET

【★ 顺序很重要:先处理,后 EOI】

    如果先发 EOI 再处理,那么在处理期间,
    同级中断就能再次打断你 → 可能造成重入问题。

    标准顺序:干活 → 发 EOI → 恢复现场 → IRET

【装中断向量】

        CLI
        MOV  AX, 0
        MOV  ES, AX
        MOV  DI, 09H * 4          ; IR1 → 类型 09H,表地址 0024H
        MOV  AX, OFFSET KEY_ISR
        MOV  ES:[DI], AX          ; IP
        MOV  AX, SEG KEY_ISR
        MOV  ES:[DI+2], AX        ; CS
        STI

    ★ 验算:09H × 4 = 24H = 0024H ✓

【常见配置速查】

        ICW1 = 13H   边沿触发、单片、要 ICW4
        ICW1 = 11H   边沿触发、级联、要 ICW4(PC 主片实际用这个)
        ICW1 = 10H   边沿触发、级联、不要 ICW4
        ICW2 = 08H   主片常用基值(PC 约定)
        ICW2 = 70H   从片常用基值
        ICW4 = 01H   8086 模式(几乎必写)
        OCW1 = 00H   全部允许
        OCW1 = FFH   全部屏蔽
        OCW2 = 20H   非指定 EOI
        OCW3 = 0AH   准备读 IRR
        OCW3 = 0BH   准备读 ISR


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 8259A 解决哪三个问题?
  2. IRR、ISR、IMR 分别记什么?分别由什么设置?
  3. 中断处理的完整流程(从外设举手到 EOI)是怎样的?
  4. 8259A 如何用一根 A0 引脚区分两组端口?
  5. PC 上主片和从片的端口地址各是多少?
  6. ICW1 的特征位是什么?为什么需要特征位?
  7. ICW2 写 08H 时,IR3 对应哪个中断类型号?
  8. 为什么 ICW2 的低 3 位必须是 0?
  9. ICW4 的 uPM 位是什么含义?写错了会怎样?
 10. OCW1 写 FEH 表示什么?写 01H 呢?
 11. 什么情况下要发两次 EOI?为什么要发 EOI?
 12. 忘了发 EOI 会有什么后果?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. ① 8 个中断源怎么接到一根 INTR(用 IR0~IR7);
     ② 同时请求时先处理谁(优先级裁决);
     ③ CPU 怎么知道是哪个中断(提供中断类型号)。
  2. IRR 记"谁举手了"(外设请求置位,响应后清);ISR 记"谁正在被服务"
     (响应时置位,EOI 时清);IMR 记"谁被屏蔽"(由 OCW1 写)。
  3. 外设举手 → IRR 置位 → 减去 IMR 屏蔽 → 裁决器挑最高优先级 →
     置入 ISR → 发 INT → CPU 发两个 INTA → 送类型号 → CPU 查表跳转 →
     服务程序处理 → 发 EOI → ISR 清零。
  4. A0=0 是主端口(写 ICW1、OCW2、OCW3,读 IRR/ISR);
     A0=1 是从端口(写 ICW2~ICW4、OCW1)。
  5. 主片 20H(A0=0)、21H(A0=1);从片 A0H、A1H。
  6. D4 = 1(ICW1 是 0001xxxx)。因为 ICW1 是初始化序列的第一个字节,
     8259A 要先认出"这是开始初始化",才能确定后面几个字节是 ICW2~4。
  7. 08H + 3 = 0BH。
  8. 因为 IR 号(0~7)就填在这 3 位里。所以基值必须是 8 的倍数。
  9. uPM=1 表示 8086/8088 模式,=0 表示 8080 模式。
     写 0 会让 8259A 按 8080 方式组织类型号,中断号全错,程序跑飞。
 10. FEH = 1111 1110 → 只允许 IR0(IR1~IR7 全屏蔽)。
     01H = 0000 0001 → 只屏蔽 IR0,允许 IR1~IR7。
 11. 从片的中断要发两次:先从片(OUT A0H)、再主片(OUT 20H)。
     因为主片不知道从片的事,两级都要通知。
     发 EOI 是为了把 ISR 对应位清 0,告诉 8259A"这次处理完了"。
 12. ISR 对应位一直是 1,8259A 认为该中断还在处理中,
     同级和更低级的中断永远不再响应。系统表现为"卡死"。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. OCW1 的方向记反
     1 = 屏蔽,0 = 允许。不是"1 = 允许"。

  2. 忘了发 EOI
     后果是同级中断再也不响应,系统像死了。

  3. ICW4 的 uPM 写成 0
     8259A 按 8080 模式工作,中断类型号全错。

  4. ICW2 写成非 8 的倍数
     低 3 位会被 IR 号占用,你写的值会被覆盖。

  5. 主片和从片的 ICW3 混用
     主片是"位图"(哪几位接了从片),从片是"编号"(我接在几号)。

  6. 先发 EOI 再处理
     可能导致同级中断重入。正确顺序是:处理完再发 EOI。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 10 篇(8255A):8255A 可以工作在"中断方式",
      它的中断请求线就接 8259A 的某根 IR。
    · 第 11 篇(8253 定时器):定时器中断通常接 IR0,
      周期性地打断 CPU(PC 上每秒 18.2 次)。
    · 第 11 篇(8251A 串口):收发完成中断接 IR3/IR4。
    · 第 12 篇(A/D):转换完成常以中断通知 CPU。

    一句话:8259A 是中断系统的"调度中心"。
    重点记三样:三个寄存器、四个 ICW、20H 这个 EOI。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    位图与结构图已设置避免跨页断开,正文按 10pt 排版。
10 8255A并行接口
========================================================================
      微机原理与接口技术 · 第 10 篇
      8255A 可编程并行接口 —— 30 分钟速通
========================================================================

前置:第 7 篇(片选译码)、第 8 篇(IN/OUT)。
这一篇讲第一颗真正的"通用"接口芯片 —— 8255A。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

前面学的都是"专用"的:8259A 只会管中断,什么的都不会。

8255A 不一样,它是【通用并行接口】:

        你想让它当输入口?可以。
        你想让它当输出口?可以。
        你想让它一半输入一半输出?可以。
        你想让它带握手信号跟打印机配合?也可以。

    怎么"编程"它?写一个控制字,它就知道该怎么干了。

所以这一篇的核心只有两个控制字:

        方式选择字(D7 = 1)      定方式、定方向
        C 口位操作字(D7 = 0)    单独翻转 C 口的某一位

    ★ 这两个字写到【同一个地址】(控制寄存器),
      全靠 D7 位区分 —— 这是 8255A 最容易搞混、也最常考的地方。

一共六站:

   第 1 站  并行与串行:8255A 的定位              (4 分钟)
   第 2 站  内部结构:三个端口被劈成两半            (5 分钟)
   第 3 站  端口地址:A1A0 决定一切                 (4 分钟)
   第 4 站  ★ 方式选择控制字                       (6 分钟)
   第 5 站  ★ C 口位操作字与 D7 的区分             (5 分钟)
   第 6 站  完整实例 + 自测                        (6 分钟)

★★ 本篇的核心数字(全部实算)★★

    端口地址(假设基址 60H):60H=A口、61H=B口、62H=C口、63H=控制口

    方式选择控制字:
        80H   全部方式0,A/B/C 全输出
        9BH   全部方式0,A/B/C 全输入
        82H   A 输出、B 输入(LED + 开关)
        90H   A 输入、B 输出
        A0H   方式1,A 口选通输出
        C0H   方式2,A 口双向

    C 口位操作字:
        01H 置 PC0  00H 复位 PC0
        07H 置 PC3  06H 复位 PC3
        0FH 置 PC7  0EH 复位 PC7


========================================================================
  第 1 站(0-4 分钟)  并行与串行
------------------------------------------------------------------------



【两种传数据的方式】

    并行:8 根线同时传 8 位 → 一次就传完,快,但线多、距离短
    串行:1 根线一位一位传 → 慢,但线少、能传远

    ★ 8255A 是【并行】接口,8251A(第 11 篇)是【串行】接口。
      同一门课里两颗都要学,就是为了对比。

【8255A 的典型用途】

        · 接 8 个 LED 做指示灯
        · 接 8 个开关读状态
        · 接数码管的段码
        · 接打印机的数据线(带握手)
        · 接继电器的控制端

    ★ 凡是"一次要传好几位、距离不远"的场合,都用并行接口。

【为什么叫"可编程"】

    因为它有控制寄存器。你写一个字节进去,
    它就按这个字节的含义改变内部连接。

    这就像一台"万能遥控器":同一颗芯片,写不同的控制字,
    就能变成输入口、输出口、或者带握手的高级口。

    ★ 这是从"硬件逻辑"到"软件配置"的关键一步 ——
      现代芯片(包括你手机里的)几乎全是这个思路。


========================================================================
  第 2 站(4-9 分钟)  内部结构:三个端口被劈成两半
------------------------------------------------------------------------



【三组端口】

        端口 A(PA7~PA0)   8 位,有自己独立的方向设置
        端口 B(PB7~PB0)   8 位,同样独立
        端口 C(PC7~PC0)   8 位,但★★ 可以拆成两个 4 位 ★★

    ★ 端口 C 的特殊之处:
      PC7~PC4 归 A 组管,PC3~PC0 归 B 组管。
      它们的方向是【分别设定】的,也可以被拆成单独的控制线。

【A 组和 B 组】

    A 组控制:管 端口A + PC 高 4 位
    B 组控制:管 端口B + PC 低 4 位

    ★ 这个"劈两半"的设计很妙:
      它让 8255A 能同时做两种不同的事。
      比如:A 组工作在方式 1(带握手),B 组还在方式 0(纯 I/O)。

【其他部分】

        数据总线缓冲器    和 CPU 的 D7~D0 相连
        读写控制逻辑      管 CS / RD / WR / A1A0 / RESET

【★ C 口的作用随方式而变】

        方式 0:C 口就是普通的 8 位 I/O(或者两个 4 位口)
        方式 1:C 口的一部分被"征用"当握手信号
                (STB、IBF、OBF、ACK、INTR)
        方式 2:C 口 5 根线当握手,只剩 3 根可用

    ★ 所以选了方式 1 或 2 以后,C 口就不能随便当 I/O 用了。
      这是考试常考的一个坑。


========================================================================
  第 3 站(9-13 分钟)  端口地址
------------------------------------------------------------------------



【四个连续地址,靠 A1A0 区分】

        A1A0 = 00  →  端口 A
        A1A0 = 01  →  端口 B
        A1A0 = 10  →  端口 C
        A1A0 = 11  →  控制寄存器

    假设片选(第 7 篇的译码输出)落在基址 60H:

        端口 A      60H
        端口 B      61H
        端口 C      62H
        控制寄存器  63H

    ★ 注意:4 个地址是【连续】的。
      所以接 8255A 时,把系统的 A1、A0 直接接到芯片的 A1、A0,
      只用 A2 以上的地址去译码。

【读写由 RD/WR 决定】

        IN  AL, 60H     读端口 A
        OUT 60H, AL     写端口 A
        OUT 63H, AL     写控制字(控制寄存器只写不读)

    ★ 控制寄存器不能读回来 —— 所以初学者常犯的错是
      "我明明写了控制字,怎么读不到?" 因为根本读不了。

【复位后的初始状态】

    RESET 之后,8255A 的所有端口都被设成【方式 0,全部输入】。

    这是一个安全默认值:默认输入,避免上电时对外设输出错误信号。

    ★ 所以如果要用端口做输出,必须先写控制字,
      否则写出去的数据根本不会出现在引脚上。


========================================================================
  第 4 站(13-19 分钟)  ★ 方式选择控制字
------------------------------------------------------------------------



【格式(D7 = 1)】

        D7    = 1        ← 标志位:表示这是方式选择字
        D6 D5 = A 组方式  00=方式0  01=方式1  1x=方式2
        D4    = A 口方向  1=输入  0=输出
        D3    = PC 高 4 位方向  1=输入  0=输出
        D2    = B 组方式  0=方式0  1=方式1
        D1    = B 口方向  1=输入  0=输出
        D0    = PC 低 4 位方向  1=输入  0=输出

    ★ 方向位:1 = 输入,0 = 输出。这个最容易记反,一定要小心。
      口诀:1 像"进"(往里),0 像"出"(往外)—— 有点牵强,
      不如直接记住"1 输入 0 输出"。

【实算六个常用值】

    ① 全部方式 0,A/B/C 全输出

        D7=1、D6D5=00、D4=0、D3=0、D2=0、D1=0、D0=0
        = 1000 0000B = 80H

    ② 全部方式 0,A/B/C 全输入

        D7=1、D6D5=00、D4=1、D3=1、D2=0、D1=1、D0=1
        = 1001 1011B = 9BH

    ③ A 输出、B 输入、C 不用(LED + 开关)

        D7=1、D6D5=00、D4=0、D3=0、D2=0、D1=1、D0=0
        = 1000 0010B = 82H

    ④ A 输入、B 输出

        1001 0000B = 90H

    ⑤ 方式 1,A 口选通输出(B 组方式 0,B 输出)

        D7=1、D6D5=01、D4=0、D3=0、D2=0、D1=0、D0=0
        = 1010 0000B = A0H

    ⑥ 方式 2,A 口双向

        D7=1、D6D5=10、D4=0、D3=0、D2=0、D1=0、D0=0
        = 1100 0000B = C0H

    ★ 注意 ⑤⑥ 里 D3 的含义:
      方式 1 时 D3 表示 PC6/PC7 是否当 I/O;
      方式 2 时 D3 表示 PC3~PC0 的方向(因为 PC7~PC3 被握手占用了)。
      这两种情况下 D3 的含义会变,考试偶尔考。

【三种方式的用途】

        方式 0  基本 I/O
            没有握手信号,就是纯粹的输入/输出。
            用于:LED、开关、数码管。

        方式 1  选通 I/O
            有握手信号(STB/IBF 或 OBF/ACK)和中断请求(INTR)。
            用于:打印机、键盘 —— 需要"你准备好了吗"的场合。

        方式 2  双向
            只有 A 口有。既能收又能发,用 5 根 C 口线做握手。
            用于:两台计算机之间的并行通信。


========================================================================
  第 5 站(19-24 分钟)  ★ C 口位操作字与 D7 的区分
------------------------------------------------------------------------



【格式(D7 = 0)】

        D7    = 0        ← 标志位:表示这是位操作字
        D6~D4 = 无关(一般写 0)
        D3 D2 D1 = 位号(000~111 对应 PC0~PC7)
        D0    = 1 置位  0 复位

【实算】

        置 PC0 = 1:位号 000,D0=1 → 0000 0001B = 01H
        复位 PC0 = 0:位号 000,D0=0 → 0000 0000B = 00H
        置 PC3 = 1:位号 011,D0=1 → 0000 0111B = 07H
        复位 PC3 = 0:位号 011,D0=0 → 0000 0110B = 06H
        置 PC7 = 1:位号 111,D0=1 → 0000 1111B = 0FH
        复位 PC7 = 0:位号 111,D0=0 → 0000 1110B = 0EH

    ★ 注意位号是从 0 开始的,而且 D3D2D1 三位放的是位号,
      不是"PC3 就在 D3"—— 这是个常见误解。

【★ 为什么要分成两个控制字】

    方式选择字:一次设定三个端口的方向,写一次管很久。
    位操作字:运行中随时单独翻转某一位,不影响其他位。

    举例:你想让 PC3 当"蜂鸣器开关",
    在程序里随时开/关它:

        MOV AL, 07H
        OUT 63H, AL      ; PC3 = 1(蜂鸣器响)
        ...
        MOV AL, 06H
        OUT 63H, AL      ; PC3 = 0(蜂鸣器停)

    ★ 如果用方式选择字去改,会连带把 A/B 口的方向也重设一遍,
      可能造成瞬间的错误输出。所以位操作字更安全。

【★ 怎么区分(最关键的一句)】

        看 D7:
            D7 = 1  →  方式选择字(定方式、定方向)
            D7 = 0  →  C 口位操作字(单独翻转一位)

    ★ 写控制字前,先问自己"D7 该是几",就不会错。

【一个常犯的错误】

    想置位 PC7,写成 OUT 63H, 80H(以为 D7 对应 PC7)。
    错!80H 的 D7=1,这是方式选择字,不是位操作字。
    正确是 0FH(位号 111、D0=1)。

    ★ 这是 8255A 最经典的陷阱题,务必记牢。


========================================================================
  第 6 站(24-30 分钟)  完整实例
------------------------------------------------------------------------

【场景:A 口接 8 个 LED,B 口接 8 个开关】

【第 1 步:确定控制字】

    A 口输出、B 口输入、C 口不用(设输出):

        D7 = 1    (方式选择)
        D6D5 = 00 (A 组方式 0)
        D4 = 0    (A 口输出,接 LED)
        D3 = 0    (PC 高 4 位输出,不用)
        D2 = 0    (B 组方式 0)
        D1 = 1    (B 口输入,接开关)
        D0 = 0    (PC 低 4 位输出,不用)

        → 控制字 = 1000 0010B = 82H

【第 2 步:初始化】

        MOV AL, 82H
        OUT 63H, AL          ; 控制口 = 基址 + 3

【第 3 步:主循环】

    循环:
        IN  AL, 61H          ; 读 B 口(开关状态)
        OUT 60H, AL          ; 写 A 口(点 LED)
        JMP 循环

    ★ 效果:拨动哪个开关,对应的 LED 就亮/灭。
      这是最简的"输入直通输出",也是所有 I/O 实验的入门第一课。

【如果还要读 C 口的某一位(比如接了一个按键)】

    8255A 没有"读状态"的特殊命令,直接读 C 口就行:

        IN   AL, 62H         ; 读整个 C 口(8 位一起读)
        TEST AL, 08H         ; 测 PC3(bit3 = 08H)
        JZ   NO_KEY          ; 为 0 说明没按下
        ...
    NO_KEY:

    ★ 注意这里用 TEST 而不是 AND(第 6 篇讲的),
      因为 TEST 不破坏 AL 的原值。

【★ 方式 1 的精髓:为什么需要握手】

    假设接打印机。CPU 把数据送到 A 口,然后呢?

        如果打印机还没准备好(还在打上一行):
            没有握手 → 数据被覆盖 → 打印出来是乱码

        有握手(方式 1):
            CPU 先看 OBF(输出缓冲满)信号
            OBF=0 才写数据
            写完 OBF 变 1,打印机取走后 ACK 拉低,OBF 恢复 0
            → 保证不丢数据

    ★ 这就是"握手"的意义:确保接收方准备好了,再送数据。

【三根握手线的分工(记忆法)】

        STB / IBF    管"进来"(输入方向)
        OBF / ACK    管"出去"(输出方向)
        INTR         管"叫 CPU"(中断请求)

    ★ 记住这三句,方式 1 就不会乱。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 8255A 有几个端口?各多少位?哪个可以被拆成两半?
  2. A 组和 B 组分别管什么?
  3. 假设基址是 200H,四个端口地址分别是多少?
  4. 8255A 复位后默认是什么状态?为什么这样设计?
  5. 方式选择控制字的 D7 是什么?方向位 1 表示输入还是输出?
  6. 写出「全部方式0、A/B/C 全输出」和「全输入」的控制字。
  7. 「A 口输出、B 口输入」的控制字是多少?怎么算出来的?
  8. C 口位操作字的格式是什么?置 PC5=1 的控制字是多少?
  9. 两种控制字写到同一个地址,靠什么区分?
 10. 想置位 PC7,写 80H 对不对?为什么?
 11. 三种工作方式各适合什么场合?方式 2 只有哪个口有?
 12. 方式 1 的三根握手线分别管什么?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 三个端口(A、B、C),都是 8 位。端口 C 可以拆成 PC7~PC4 和 PC3~PC0 两个 4 位。
  2. A 组管端口 A + PC 高 4 位;B 组管端口 B + PC 低 4 位。
  3. 200H(A)、201H(B)、202H(C)、203H(控制寄存器)。
  4. 全部端口都是【方式 0、输入】。这是个安全默认值,
     避免上电时对外设输出错误信号。
  5. D7 = 1。方向位 1 = 输入,0 = 输出。
  6. 全输出 = 80H(1000 0000);全输入 = 9BH(1001 1011)。
  7. 82H。推导:D7=1、D6D5=00、D4=0(A输出)、
     D3=0、D2=0、D1=1(B输入)、D0=0 → 1000 0010B。
  8. D7=0、D6~D4 无关、D3D2D1=位号、D0=1置位/0复位。
     置 PC5 = 位号 101 + D0=1 → 0000 1011B = 0BH。
  9. 靠 D7 位:D7=1 是方式选择字,D7=0 是 C 口位操作字。
 10. 不对。80H 的 D7=1,是方式选择字。置位 PC7 应该是 0FH
     (位号 111、D0=1)。
 11. 方式 0:基本 I/O(LED、开关);方式 1:选通 I/O(打印机、键盘);
     方式 2:双向(两台计算机通信)。方式 2 只有 A 口有。
 12. STB/IBF 管输入方向;OBF/ACK 管输出方向;INTR 管中断请求。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 方向位记反
     1 = 输入,0 = 输出。写反了整个接口的行为都反了。

  2. 两种控制字搞混
     D7=1 是方式字,D7=0 是位操作字。想置位 PC7 却写 80H 是最经典的错。

  3. 忘了先写控制字就用端口
     复位后默认全是输入。不写控制字,输出根本没反应。

  4. 以为控制寄存器能读回来
     它只写不读。写完就没了,读不到。

  5. 选了方式 1 还拿 C 口当普通 I/O
     方式 1/2 会征用 C 口的部分引脚当握手信号。

  6. 位操作字的位号理解错
     D3D2D1 是【位号】(0~7),不是"PC3 对应 D3"。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 11 篇(8253 定时器):同样是"写控制字 + 读写数据"的套路。
      三颗芯片(8259A、8255A、8253)的编程模式高度一致。
    · 第 11 篇(8251A 串口):它是串行版,正好和本篇对比。
    · 第 12 篇(A/D、D/A):转换结果常通过 8255A 读进来。
    · 实际工程:8255A 是"并口"的原型,现在的 GPIO 口
      就是它的现代版(也是"写方向寄存器 + 读写数据寄存器")。

    一句话:8255A 是"可编程接口"的样板。
    重点记两样:两个控制字的格式、D7 怎么区分。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    位图与结构图已设置避免跨页断开,正文按 10pt 排版。
11 8253定时器与8251A串口
========================================================================
      微机原理与接口技术 · 第 11 篇
      8253 定时/计数器 与 8251A 串行接口 —— 30 分钟速通
========================================================================

前置:第 10 篇(8255A 的"写控制字"套路)。
这一篇讲两颗芯片:8253(定时)和 8251A(串行通信)。
重点在 8253 的【计数初值】—— 这是考试必考、也最容易算错的地方。


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

先问一个实际问题:怎么让 CPU "等一会儿"?

    办法一:软件延时循环
        MOV CX, 1000
    L:  LOOP L
        缺点:CPU 被占死(第 8 篇讲的"忙等待"),而且时间不准
              (受时钟频率、指令长度影响)

    办法二:用【硬件定时器】
        写一个初值进去,它自己数,数完通知 CPU。
        CPU 写完就能去干别的,时间精确到时钟周期。

8253 就是干这个的。它内部有【三个独立的减法计数器】。

这一篇分两半:

    前半(8253):怎么设初值、六种方式怎么选
    后半(8251A):串行通信怎么把字节变成比特流

一共六站:

   第 1 站  8253 的三个计数器                        (4 分钟)
   第 2 站  控制字:一个字节定四件事                 (5 分钟)
   第 3 站  ★ 计数初值怎么算、怎么写               (6 分钟)
   第 4 站  六种工作方式的波形                       (6 分钟)
   第 5 站  8251A 串行通信基础                      (5 分钟)
   第 6 站  完整实例 + 自测                        (4 分钟)

★★ 本篇的核心数字(全部实算)★★

    端口(PC 上):40H=计数器0、41H=计数器1、42H=计数器2、43H=控制口

    控制字:
        36H   计数器0,先低后高,方式3(方波)
        30H   计数器0,先低后高,方式0
        74H   计数器1,先低后高,方式2
        B6H   计数器2,先低后高,方式3(PC 扬声器)
        91H   计数器2,只读写低字节,方式0,BCD

    初值计算:
        1MHz 时钟 → 1kHz 输出:初值 = 1,000,000 ÷ 1,000 = 1000 = 03E8H
        写入顺序:【先 E8H(低),后 03H(高)】★ 反了就全错

    串行:8N1 一帧 10 位,9600 波特 ≈ 960 字节/秒


========================================================================
  第 1 站(0-4 分钟)  8253 的三个计数器
------------------------------------------------------------------------



【三个独立计数器】

    计数器 0、计数器 1、计数器 2 —— 每个都是 16 位【减法】计数器。

    每个有三根线:

        CLK   时钟输入(每来一个脉冲减 1)
        GATE  门控(高电平允许计数,低电平暂停)
        OUT   输出(减到 0 时产生动作)

    ★ 三个计数器完全独立,可以同时按不同方式工作。
      比如计数器 0 做 1kHz 方波、计数器 1 做 10ms 定时、计数器 2 驱动扬声器。

【端口地址】

        A1A0 = 00  →  计数器 0      40H
        A1A0 = 01  →  计数器 1      41H
        A1A0 = 10  →  计数器 2      42H
        A1A0 = 11  →  控制寄存器    43H

    ★ 和 8255A 一样:四个连续地址,A1A0 区分。

【PC 上的三个计数器都在干什么】

        计数器 0  系统定时器(每秒 18.2 次,驱动时钟中断 INT 08H)
        计数器 1  内存刷新(DRAM 需要定时刷新)
        计数器 2  驱动扬声器(改变频率就发出不同音调)

    ★ 这三个用途是 PC 的固定约定,考试常考。

【为什么是"减法"计数器】

    装一个初值 N,每个时钟减 1:
        N → N−1 → N−2 → … → 1 → 0
    减到 0 的时候产生输出信号。

    所以它天然就是一个【分频器】:输入 N 个脉冲,输出 1 个事件。

    ★ 这就是初值公式的由来:
      初值 = 输入频率 ÷ 输出频率
      比如 1MHz 输入、要 1kHz 输出,就是每 1000 个时钟出一次 →
      初值 = 1000


========================================================================
  第 2 站(4-9 分钟)  控制字
------------------------------------------------------------------------



【格式:一个字节,四种选择】

        D7 D6 | D5 D4 | D3 D2 D1 | D0
        SC1 SC0| RW1 RW0| M2 M1 M0 | BCD

    ① SC(选计数器)
        00 = 计数器 0
        01 = 计数器 1
        10 = 计数器 2
        11 = 非法(不能用)

    ② RW(读写方式)
        00 = 锁存当前计数值(读出来看)
        01 = 只读写【低字节】
        10 = 只读写【高字节】
        11 = 【先低字节后高字节】← 最常用

    ③ M(工作方式)
        000 ~ 101 对应方式 0 ~ 5
        110、111 不用

    ④ BCD(计数制)
        0 = 二进制(初值范围 0 ~ 65535)
        1 = BCD 码(初值范围 0 ~ 9999)

【实算几个常用值】

    ① 计数器 0,先低后高,方式 3,二进制

        SC=00、RW=11、M=011、BCD=0
        = 00 11 011 0 = 0011 0110B = 36H

        ★ 这是最常用的一个控制字,务必记住。

    ② 计数器 1,先低后高,方式 2,二进制

        SC=01、RW=11、M=010、BCD=0
        = 01 11 010 0 = 0111 0100B = 74H

    ③ 计数器 2,先低后高,方式 3,二进制(PC 扬声器)

        SC=10、RW=11、M=011、BCD=0
        = 10 11 011 0 = 1011 0110B = B6H

    ④ 计数器 2,只读写低字节,方式 0,BCD

        SC=10、RW=01、M=000、BCD=1
        = 10 01 000 1 = 1001 0001B = 91H

【★ 一个容易算错的地方】

    M(方式)字段占的是 D3 D2 D1 三位,不是 D3 D2 D1 之外的。

    所以算的时候是:(M << 1),不是 (M << 3)。

    ★ 我第一次算的时候就错了:把方式 3 写成 3 << 3 = 18H,
      结果 36H 变成了 38H。差一位,全错。

    正确算法(方式 3、计数器 0、先低后高、二进制):

        0011 0000   ← RW=11 在 D5D4 → 0x30
      | 0000 0110   ← M=011 在 D3D2D1 → 3<<1 = 0x06
      -----------
        0011 0110   = 36H ✓

    校验:D5D4 = 11 ✓、D3D2D1 = 011 ✓、D0 = 0 ✓


========================================================================
  第 3 站(9-15 分钟)  ★ 计数初值:算和写
------------------------------------------------------------------------



【公式】

        初值 = 输入时钟频率 ÷ 想要的输出频率

    ★ 是【除】,不是乘。这个方向别搞反。

【实算表】

        时钟        想要的输出      初值        十六进制      写入顺序
        1MHz        1kHz            1000        03E8H         先 E8H 后 03H
        2MHz        1kHz            2000        07D0H         先 D0H 后 07H
        1MHz        100Hz           10000       2710H         先 10H 后 27H
        4MHz        2kHz            2000        07D0H         先 D0H 后 07H
        1.193MHz    18.2Hz          65536       0000H(写 0)   写 00H 00H

【★ 三个必须记住的点】

    ① 初值 = 时钟 ÷ 输出频率(除,不是乘)

    ② 16 位计数器最大 65535,但【写 0 表示 65536】
       所以最大分频是 65536。

       ★ PC 上就是用它:1.193182MHz ÷ 65536 = 18.2Hz
         这就是那个"每秒 18.2 次"的来历。

    ③ 写入顺序是【先低字节,后高字节】(小端)

       ★ 这条最容易错。第 3 篇讲过小端,这里又出现一次。

【实算:1MHz 产生 1kHz 方波】

        初值 = 1,000,000 ÷ 1,000 = 1000 = 03E8H
        低字节 = E8H = 232
        高字节 = 03H = 3
        校验:3 × 256 + 232 = 1000 ✓

【完整代码】

        MOV AL, 36H          ; 计数器0,先低后高,方式3,二进制
        OUT 43H, AL          ; 写控制寄存器

        MOV AL, 0E8H         ; 低字节 = 232
        OUT 40H, AL          ; 第一次写:低字节

        MOV AL, 03H          ; 高字节 = 3
        OUT 40H, AL          ; 第二次写:高字节

    ★ 注意 40H 这个端口写了【两次】。
      因为控制字的 RW=11 表示"先低后高",8253 会自己记住
      "第一次是低字节,第二次是高字节"。

【如果写反了会怎样(实算)】

    写反:先写 03H(被当成低字节),再写 E8H(被当成高字节)

        实际初值 = 232 × 256 + 3 = 59395
        输出频率 = 1,000,000 ÷ 59395 ≈ 16.8Hz

    ★ 期望 1000Hz,实际 16.8Hz —— 慢了 60 倍。
      而且现象很迷惑(以为是晶振坏了)。

    这就是为什么"先低后高"必须记牢。


========================================================================
  第 4 站(15-20 分钟)  六种工作方式
------------------------------------------------------------------------



    六种方式的区别就三件事:
        OUT 什么时候变、变几次、要不要 GATE 触发。

【方式 0:计数结束产生中断】

    写入初值后开始计数,减到 0 时 OUT 从低变高。
    OUT 保持高电平,直到重新写初值。

    ★ 单次,不自动重装。用途:延时、计数。

【方式 1:可编程单稳】

    GATE 上升沿触发后,OUT 变低;计完 N 个时钟,OUT 变高。
    宽度 = N × 时钟周期。

    ★ 单次。用途:产生固定宽度的脉冲。

【方式 2:频率发生器】

    周期性输出。每个周期 OUT 低一个时钟,其余时间为高。

    ★ 自动重装(周期性)。这是最常用的方式之一。
      用途:产生连续脉冲、分频。

【方式 3:方波发生器】

    输出对称方波,周期 = 2 × 初值 × 时钟周期。
    (因为它是"计一半翻转一次")

    ★ 自动重装。最常用 —— 驱动扬声器、产生时钟。
      ★ 注意:方式 3 的输出周期是【2 倍】初值,
        而方式 2 是【1 倍】初值。这是常考的区别点。

【方式 4:软件触发选通】

    写初值后开始计数,减到 0 时 OUT 低【一个时钟】,然后恢复高。

    ★ 单次。用途:产生一个窄脉冲(触发信号)。

【方式 5:硬件触发选通】

    和方式 4 一样,但由 GATE 上升沿触发(不是写初值触发)。

    ★ 单次。用途:外部事件触发一个窄脉冲。

【★ 记忆要点】

        自动重装(周期性):方式 2、方式 3
        单次:方式 0、1、4、5

        要"连续输出" → 用 2 或 3
        要"延时/计次" → 用 0
        要"一个脉冲"  → 用 1、4、5

    ★ 方式 2 和 3 的周期差别:
      方式 2:输出周期 = 初值 × T_clk
      方式 3:输出周期 = 2 × 初值 × T_clk
      所以同样要 1kHz,方式 2 初值 1000、方式 3 也是 1000?
      不是!方式 3 的方波一个完整周期包含"高 N 个 + 低 N 个",
      所以要做到 1kHz,初值仍是 1000(因为它的半周期是 N×T)。
      ★ 这一点各教材表述略有差异,做题时以题目的定义为准。

【GATE 的作用】

        GATE = 1  允许计数
        GATE = 0  暂停计数

    ★ 所以 GATE 可以当"启动/停止"开关用。
      方式 1 和 5 必须靠 GATE 的上升沿触发。


========================================================================
  第 5 站(20-25 分钟)  8251A 串行通信基础
------------------------------------------------------------------------



【串行 vs 并行】

    并行(8255A):8 根线同时传 8 位,快,但线多、距离短
    串行(8251A):1 根线一位一位传,慢,但线少、能传几公里

【异步串行的帧格式】

    串行没有"地址",接收方怎么知道一帧从哪开始?
    靠【起始位】和【停止位】划分:

        起始位(1 位,低电平) + 数据位(5~8 位) + 校验位(0/1 位) + 停止位(1/1.5/2 位)

    最常见的是【8N1】:
        1 起始 + 8 数据 + 0 校验 + 1 停止 = 10 位/字节

    ★ 数据位是【低位先传】(D0 在前)——
      这又是"小端"思想,和前面讲的一致。

    ★ 空闲时线上是高电平。起始位是一个下降沿,
      接收方靠这个下降沿和发送方"对齐"(这叫"同步")。

【波特率】

        波特率 = 每秒传的【位】数(bit/s),不是字节数

    所以 9600 波特传 8N1 数据:
        9600 ÷ 10 = 960 字节/秒

    ★ 这个换算经常考:「9600 波特每秒传多少字节?」答 960。

【8251A 需要外部时钟】

        时钟频率 = 波特率 × 波特率因子

    因子可以是 1、16、64(最常用 16)。

        例:波特率 1200、因子 16 → 需要 19200Hz 的时钟

【8251A 的编程特点】

    它有两个控制字,都要写到同一个端口(但不是同时):

        模式字(Mode Word)   定波特率因子、字符长度、校验、停止位
        命令字(Command Word) 定发送/接收使能、复位、出错复位

    ★ 顺序:先写模式字,再写命令字。模式字必须紧跟复位之后写。

    判断"什么时候该写模式字":
        8251A 有个内部标志,复位后第一次写的是模式字,
        之后写的都是命令字。

【8251A 的引脚(了解)】

        TxD   发送数据(串行输出)
        RxD   接收数据(串行输入)
        TxC   发送时钟
        RxC   接收时钟
        TxRDY 发送准备好(可接中断)
        RxRDY 接收准备好(可接中断)
        SYNDET 同步检测(同步方式用)

    ★ TxRDY / RxRDY 通常接到 8259A 的某个 IR 上(第 9 篇)。


========================================================================
  第 6 站(25-30 分钟)  完整实例
------------------------------------------------------------------------

【场景:用 8253 计数器 2 驱动扬声器,发出 1kHz 的声音】

    假设 8253 的时钟输入是 1MHz,要输出 1kHz 方波。

【第 1 步:算初值】

        初值 = 1,000,000 ÷ 1,000 = 1000 = 03E8H
        低字节 = E8H,高字节 = 03H

【第 2 步:写控制字】

        计数器 2、先低后高、方式 3、二进制
        = 10 11 011 0 = B6H

【第 3 步:写初值】

        MOV AL, 0B6H
        OUT 43H, AL          ; 控制字

        MOV AL, 0E8H
        OUT 42H, AL          ; 低字节(注意端口是 42H,不是 40H)

        MOV AL, 03H
        OUT 42H, AL          ; 高字节

    ★ 注意端口:计数器 0 是 40H、计数器 1 是 41H、计数器 2 是 42H。
      写错了就写到别的计数器上去了。

【第 4 步:(PC 上)打开扬声器门控】

        IN   AL, 61H         ; 读 PC 的 8255 端口 B(控制扬声器)
        OR   AL, 03H         ; 打开扬声器和定时器门控
        OUT  61H, AL
        MOV  AL, 0B6H
        OUT  43H, AL
        MOV  AL, 0E8H
        OUT  42H, AL
        MOV  AL, 03H
        OUT  42H, AL

    ★ 这是 PC 上用汇编"发声"的经典代码,很多教材都有。

【另一个实例:产生 10ms 延时】

    假设时钟 1MHz(周期 1μs),要 10ms:

        初值 = 10ms ÷ 1μs = 10000 = 2710H

        控制字:计数器 0、先低后高、方式 0、二进制 = 30H

        MOV AL, 30H
        OUT 43H, AL
        MOV AL, 10H          ; 低字节
        OUT 40H, AL
        MOV AL, 27H          ; 高字节
        OUT 40H, AL

    查询等待:
        WAIT: IN  AL, 40H    ; 读计数器当前值
              ...            ; 判断到 0 了没

    ★ 或者更简单:把 OUT0 接到某个输入口,用查询判断。

【★ 三个"必须先算清楚"的量】

    ① 初值(除出来的)
    ② 控制字(按四位一组拼出来的)
    ③ 端口(哪个计数器、哪个地址)

    这三样任何一个错了,现象都很迷惑(不响、频率不对、没反应)。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 8253 有几个计数器?各多少位?是加还是减?
  2. 每个计数器有哪三根线?各干什么?
  3. 8253 的四个端口地址(基址 40H)分别是什么?
  4. 控制字的四组字段分别是什么?
  5. 控制字 36H 表示什么?逐位说明。
  6. 写出「计数器 1、先低后高、方式 2、二进制」的控制字。
  7. 计数初值的公式是什么?1MHz 要 2kHz,初值是多少?
  8. 初值 03E8H 写入时,先写哪个字节?写反会怎样?
  9. 写 0 表示什么?最大分频是多少?
 10. 哪几种方式会自动重装?方式 2 和方式 3 有什么区别?
 11. 8N1 一帧多少位?9600 波特每秒传多少字节?
 12. 8251A 的模式字和命令字有什么区别?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 三个(计数器 0/1/2),都是 16 位【减法】计数器。
  2. CLK 时钟输入、GATE 门控、OUT 输出。
  3. 40H 计数器 0、41H 计数器 1、42H 计数器 2、43H 控制寄存器。
  4. SC1SC0(选计数器)、RW1RW0(读写方式)、M2M1M0(工作方式)、BCD(计数制)。
  5. 36H = 0011 0110:SC=00(计数器 0)、RW=11(先低后高)、
     M=011(方式 3)、BCD=0(二进制)。
  6. SC=01、RW=11、M=010、BCD=0 → 01 11 010 0 = 0111 0100B = 74H。
  7. 初值 = 输入时钟频率 ÷ 输出频率。1MHz ÷ 2kHz = 500 = 01F4H。
  8. 先写低字节 E8H,再写高字节 03H。
     写反会得到初值 232×256+3 = 59395,输出频率变成约 16.8Hz。
  9. 写 0 表示 65536(因为 16 位计数器最大计到 65535,写 0 是特殊约定)。
     最大分频 65536。
 10. 方式 2 和方式 3 会自动重装(其余四种是单次)。
     方式 2 输出周期性窄脉冲(低一个时钟),方式 3 输出对称方波。
 11. 8N1 = 1 起始 + 8 数据 + 0 校验 + 1 停止 = 10 位。
     9600 ÷ 10 = 960 字节/秒。
 12. 模式字定通信格式(波特率因子、字符长度、校验、停止位);
     命令字定操作(允许发送/接收、复位等)。
     必须先写模式字,再写命令字。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 控制字的 M 字段算错位
     M 在 D3D2D1,要 M << 1,不是 M << 3。
     方式 3 配计数器 0 应该是 36H,不是 38H。

  2. 初值用乘的
     初值 = 时钟 ÷ 输出频率。用乘会得到荒唐的大数。

  3. 写入顺序反了
     必须先低字节后高字节。反了输出频率会差几十倍。

  4. 端口写错
     计数器 0/1/2 分别是 40H/41H/42H,控制字写 43H。

  5. 以为写 0 是"不计数"
     写 0 表示 65536,是最大值,不是停止。

  6. 方式 2 和方式 3 混为一谈
     方式 2 是窄脉冲,方式 3 是方波。要驱动扬声器用方式 3。


========================================================================
  这一篇和后面的关系
========================================================================

    · 第 12 篇(A/D、D/A):A/D 转换需要定时启动,
      常用 8253 产生采样脉冲。
    · 第 9 篇(8259A):8253 的 OUT 可以接 8259A 的 IR,
      定时到了就产生中断 —— 这就是 PC 上 INT 08H 的来历。
    · 实际工程:现在的定时器(如 ARM 的 Timer、PC 的 PIT)
      原理完全一样:装初值 → 递减 → 到 0 时产生事件。
      8253 的六种方式,现代芯片里仍然是这几种。

    一句话:8253 是"硬件定时"的样板。
    重点记三样:控制字 36H、初值 = 时钟÷输出、先低后高。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    波形图与位图已设置避免跨页断开,正文按 10pt 排版。
12 ADDA与总线技术
========================================================================
      微机原理与接口技术 · 第 12 篇
      A/D、D/A 转换与总线技术 —— 30 分钟速通
========================================================================

前置:第 8 篇(I/O)、第 10 篇(8255A)。
这是全书的最后一篇。它回答一个问题:
计算机怎么和「真实世界」打交道?


========================================================================
  全章地图:这一篇到底在讲什么
========================================================================

前面十一篇,输入输出处理的全是已经在 0/1 世界里的东西:
键盘给出数字、LED 接收数字、打印机收字节。

可真实世界是连续变化的:温度、声音、压力、光线 ——
它们都是"模拟量",不是 0 和 1。

要让计算机处理它们,必须经过两次翻译:

        模拟 → 数字:A/D 转换(ADC)
        数字 → 模拟:D/A 转换(DAC)

这两颗芯片就是"数字世界"和"物理世界"的边界。

另外,这一篇还要讲【总线】——
CPU 和所有部件之间的"公共通道",
它决定了整个系统能跑多快。

一共六站:

   第 1 站  两个世界:A/D 和 D/A 的位置            (4 分钟)
   第 2 站  D/A 转换的原理与分辨率                 (5 分钟)
   第 3 站  ★ A/D 的三步:采样、量化、编码        (6 分钟)
   第 4 站  ADC0809 与 CPU 的接口                  (5 分钟)
   第 5 站  总线与总线周期                         (6 分钟)
   第 6 站  综合练习 + 全书收尾                    (4 分钟)

★★ 本篇的核心数字(全部实算)★★

    1 LSB = 满量程 ÷ 2^n
    8 位、5V 满量程:1 LSB = 5 ÷ 256 = 19.53 mV
    12 位、5V 满量程:1 LSB = 5 ÷ 4096 = 1.22 mV

    A/D:D = Vin × 2^n ÷ Vref
        2.5V → 2.5 × 256 ÷ 5 = 128 = 80H
    D/A:Vout = D × Vref ÷ 2^n
        80H(=128) → 128 × 5 ÷ 256 = 2.5V

    量化误差 = ±0.5 LSB = ±9.77 mV(8 位 5V)
    8 位 D/A 满量程输出 = 255 × 5/256 = 4.98V(不是 5V!)

    8086 总线周期 = 4 个 T,5MHz 时 = 800ns


========================================================================
  第 1 站(0-4 分钟)  两个世界
------------------------------------------------------------------------



【两个世界的差别】

        物理世界      连续变化,无限精细(温度可以是 23.4567℃)
        数字世界      离散的,有限精度(只能是 0~255 的某个整数)

    ★ 这个"从无限到有限的转换"就是所有误差的来源。
      你不可能用有限的数字完全表示连续的量 —— 只能"足够接近"。

【两条路】

        输入方向:传感器 → A/D → CPU
            例:温度传感器输出电压 → ADC0809 → 数字量 → CPU 判断

        输出方向:CPU → D/A → 执行器
            例:CPU 算出控制量 → DAC0832 → 电压 → 驱动电机

    ★ 一个完整的控制系统,两条路都要有。

【为什么不能直接接】

    传感器输出的是"电压"(模拟量),CPU 只认"数字量"。
    两者之间必须有个翻译。

    ★ 这和 8255A 的角色有点像(都是"翻译"),
      但 8255A 翻译的是"并行/电平",A/D 翻译的是"模拟/数字"。


========================================================================
  第 2 站(4-9 分钟)  D/A 转换
------------------------------------------------------------------------

【原理:按位"加电压"】

    一个 8 位 D/A 里有 8 个"开关",每个对应一位:

        D7(最高位)→ 贡献 Vref/2
        D6           → 贡献 Vref/4
        D5           → 贡献 Vref/8
        …
        D0(最低位)→ 贡献 Vref/256

    所以:Vout = Vref × (D7/2 + D6/4 + … + D0/256)
              = Vref × D / 256

    ★ 就是"二进制转模拟电压",本质上是一个加权求和。

【★ 分辨率:1 个 LSB 是多少伏】

        1 LSB = 满量程电压 ÷ 2^n

    实算(8 位、5V 基准):

        1 LSB = 5 ÷ 256 = 0.01953125 V = 19.53 mV

    也就是说:数字量加 1,输出只抬高 19.53mV。
    这个"台阶高度"就是 D/A 的分辨率。



【实算几个转换】

        数字量        输出(Vref = 5V)
        00H (0)       0 V
        40H (64)      64 × 5 ÷ 256 = 1.25 V
        80H (128)     128 × 5 ÷ 256 = 2.5 V
        C0H (192)     192 × 5 ÷ 256 = 3.75 V
        FFH (255)     255 × 5 ÷ 256 = 4.98 V

    ★ 注意最后一行:8 位 D/A 的最大输出不是 5V,而是 4.98V。
      因为 256 个台阶里,最高只能走到第 255 个台阶。
      差的那 1 个 LSB 就是"量化误差"。

    ★ 这是个常考的点:"8 位 5V 的 D/A,输出最大值是多少?"
      答案:255/256 × 5 = 4.98V,不是 5V。

【DAC0832:8 位乘法型 D/A】

    特点:
        · 8 位分辨率
        · 可以"乘法"工作(Vref 可变,输出与 Vref 成正比)
        · 内部有两级锁存器(输入锁存 + DAC 寄存器)→ 可以双缓冲
        · 建立时间约 1μs

    引脚(了解):
        DI7~DI0     数字量输入
        CS、WR1     第一级锁存控制
        WR2、XFER   第二级锁存控制
        IOUT1/IOUT2 电流输出(需要外接运放转成电压)
        VREF        基准电压

    ★ 为什么要两级锁存?
      为了"双缓冲":可以让 CPU 先把数据算好放进第一级,
      等需要的时候再一起送到第二级 —— 多路 D/A 可以同时更新。
      这对"波形发生器"很重要(多路输出要同步)。

【D/A 的精度 vs 分辨率】

        分辨率    能分多细(由位数决定,理论值)
        精度      实际有多准(受电阻误差、温漂影响)

    ★ 一个 12 位 D/A 可能因为电阻不准,精度还不如好的 8 位。
      "位数高 = 精度高"是个常见误解。


========================================================================
  第 3 站(9-15 分钟)  ★ A/D 的三步
------------------------------------------------------------------------

    A/D 比 D/A 复杂,因为它要把"连续"变成"离散",分三步。



【① 采样:隔一段时间取一个点】

    用采样保持器(SHA)在某一瞬间"抓住"电压,并保持住,
    让 ADC 有时间转换。

    ★ 为什么需要"保持"?
      因为转换要时间(100μs),这期间电压如果变了,转换结果就没意义。

【★ 采样定理(奈奎斯特)】

        fs ≥ 2 × fmax

    采样率必须大于信号最高频率的 2 倍。

    为什么?如果采得太慢,高频信号会被"误认"成低频信号 ——
    这叫【混叠】,而且一旦混叠就再也分不开了。

    实例:
        电话语音最高 3400Hz → 采样 8kHz(>2×3400)✓
        CD 音频最高 20kHz   → 采样 44.1kHz(>2×20k)✓

【② 量化:把点对齐到格子上】

    采到的电压是连续的,但 ADC 只能输出有限的整数。
    所以要把电压"对齐"到最近的格子上。

    ★ 这一步产生的误差叫【量化误差】:±0.5 LSB

    实算(8 位、5V):
        1 LSB = 19.53 mV
        量化误差 = ±0.5 × 19.53 = ±9.77 mV

    ★ 这个误差原理上无法消除,只能靠提高位数减小。
      位数每加 1,格子数翻倍,误差减半。

        8 位:±9.77 mV
        10 位:±2.44 mV
        12 位:±0.61 mV

【③ 编码:转成二进制】

    把格子的编号转成二进制,得到数字量 D。

        公式:D = Vin × 2^n ÷ Vref

    实算:

        输入        8 位(5V)        12 位(5V)
        0.00 V      0    (00H)        0    (000H)
        1.25 V      64   (40H)        1024 (400H)
        2.50 V      128  (80H)        2048 (800H)
        3.75 V      192  (C0H)        3072 (C00H)
        5.00 V      255  (FFH)        4095 (FFFH)

    ★ 校验:2.5V 正好是满量程一半,8 位得 128 = 80H ✓
      注意 5.00V 只得 255(FFH),不是 256 —— 和 D/A 那边对应。

【★ A/D 和 D/A 是一对"反函数"】

        A/D:Vin → D,公式 D = Vin × 2^n / Vref
        D/A:D → Vout,公式 Vout = D × Vref / 2^n

    所以把 2.5V 送进 A/D 得 80H,再把 80H 送进 D/A 又得回 2.5V ✓
    但如果中间有量化误差,来回一趟就会差一点。

【逐次逼近法:ADC 常用的转换方法】

    ADC0809 用的是"逐次逼近"(像一个猜数字的游戏):

        先猜一半(1000 0000 = 128),比较:
            D/A 输出 < 输入? → 猜小了,这一位保留 1,下一位再猜
            D/A 输出 > 输入? → 猜大了,这一位改回 0

        8 位就要猜 8 次,每次一个时钟 → 8 个时钟完成(实际要 64 个时钟
        包括控制开销)。

    ★ 这就像"猜 1~100 的数,每次问'比 50 大吗'",8 次就能定到 1 的精度。


========================================================================
  第 4 站(15-20 分钟)  ADC0809 与 CPU 的接口
------------------------------------------------------------------------



【ADC0809 的规格】

        8 位分辨率
        8 个模拟输入通道(IN0~IN7)
        转换时间约 100μs(时钟 640kHz 时 64 个周期)
        输入电压范围 0~5V(单极性)
        需要外部时钟

【★ 只用一个 ADC 就有 8 个通道:靠多路开关】

    内部有一个 8 选 1 的模拟开关,用 C、B、A 三根线选通道:

        CBA = 000 → IN0      CBA = 100 → IN4
        CBA = 001 → IN1      CBA = 101 → IN5
        CBA = 010 → IN2      CBA = 110 → IN6
        CBA = 011 → IN3      CBA = 111 → IN7

    ★ 这是"分时复用":8 个通道共用一套转换电路,
      所以同一时刻只能转换一个通道。

【四步操作流程】

    ① 用 CBA 选中要转换的通道
    ② 给 START 一个正脉冲,启动转换
    ③ 等 EOC(转换结束)变高
       ★ 两个办法:查询(一直读 EOC)或接中断(EOC 接 8259A)
    ④ 给 OE 高电平,把结果送上数据总线,CPU 用 IN 读走

【代码示例(假设端口 80H)】

    ; ① 选通道 2(CBA = 010)
        MOV AL, 02H
        OUT 80H, AL          ; 写到通道选择口

    ; ② 启动(同一个端口再写一次,产生 START 脉冲)
        OUT 80H, AL

    ; ③ 查询等待
    WAIT:
        IN  AL, 81H          ; 读状态口(EOC)
        TEST AL, 01H         ; 测 EOC 位
        JZ  WAIT             ; 还没完成,继续等

    ; ④ 读结果
        IN  AL, 80H          ; 读数据

    ★ 注意:端口地址 80H/81H 是假设的,实际由第 7 篇的译码器决定。

【★ 100μs 对 CPU 来说有多长】

    8086 在 5MHz 下,一条普通指令约 1μs。
    100μs 相当于 100 条指令!

    ★ 所以"查询等待"会浪费大量 CPU 时间。
      更好的办法是把 EOC 接到 8259A 的中断输入(第 9 篇),
      转换期间 CPU 去干别的,转换完成后中断通知。

    ★ 这就是第 8 篇讲的"三种 I/O 方式"在这里的实际应用:
      少量、偶尔 → 查询;频繁、实时 → 中断;高速大批量 → DMA。


========================================================================
  第 5 站(20-26 分钟)  总线与总线周期
------------------------------------------------------------------------

【总线的三组线】

        地址总线 AB   单向(CPU → 外部),20 位 → 1MB
        数据总线 DB   双向,16 位 → 一次传 2 字节
        控制总线 CB   每根方向不同:
                        RD/WR/M-IO 是输出(CPU 发出)
                        READY/INTR 是输入(外部送入)

    ★ 总线是"公共通道":所有部件都挂在上面,
      但同一时刻只能有一个"说话"(否则数据冲突,第 7 篇的片选就是干这个)。

【★ 8086 的地址数据复用】

    8086 为了省引脚,把地址和数据的低 16 位【复用】同一组引脚
    (AD0~AD15):

        T1 时,这组线上是【地址】
        T2~T4 时,这组线上是【数据】

    ★ 怎么区分?靠 ALE 信号:
      T1 时 ALE 有效(高电平),告诉外部"现在线上是地址,快锁存!"

    ★ 所以 8086 的系统里必须有【地址锁存器】(74LS373),
      把 T1 时出现的地址"抓"下来存住。这是 8086 最小系统的必备器件。

【总线周期:4 个 T】



    以 5MHz 时钟为例,T = 200ns:

        T1  送地址 + ALE 有效(外部锁存地址)
        T2  送读/写信号,总线从地址阶段转到数据阶段
        T3  数据稳定,CPU 在末尾采样
        T4  结束,释放总线

    一个总线周期 = 4 × 200ns = 800ns

    ★ 所以 8086 一秒钟最多做 125 万次访存(1 ÷ 800ns)。
      这就是它当年的"速度"。

【等待周期 Tw】

    如果存储器或外设太慢,赶不上 T3 的采样,
    它可以拉低 READY 信号,CPU 就会在 T3 和 T4 之间
    插入若干个等待周期 Tw,把总线周期拉长。

    ★ 这就是"CPU 等外设"的硬件实现。
      第 4 站的 ADC 转换要 100μs,如果让 CPU 硬等,
      就要插 125 个 Tw —— 所以更该用中断或查询,而不是让总线等。

【现代总线(了解)】

    8086 时代的总线(ISA)早就不够用了。现在的总线分层:

        PCI / PCIe   主板内部的高速总线(显卡、网卡)
        USB          外部通用串行总线(键盘、U 盘)
        SATA         硬盘串行总线
        DDR          内存总线(每代速度翻倍)

    ★ 但基本原理没变:
      · 都有地址线、数据线、控制线(或者用包/事务代替)
      · 都要仲裁(谁能用总线)
      · 都有时序规范(什么时候送地址、什么时候采样)

    ★ 现代总线的两个巨大改进:
      ① 串行化(一根线跑几个 Gbps,比并行 32 根还快 ——
         因为并行线之间有"偏斜",频率一高就对不齐)
      ② 包交换(把地址和数据打包成"事务",而不是靠固定时序)
         这就是 PCIe 和 8086 总线的本质区别。


========================================================================
  第 6 站(26-30 分钟)  综合练习 + 全书收尾
------------------------------------------------------------------------

【练习一:算分辨率】

    一个 10 位 A/D,基准 5V,1 LSB 是多少?

        1 LSB = 5 ÷ 2^10 = 5 ÷ 1024 = 4.88 mV

【练习二:算数字量】

    12 位 A/D,Vref = 5V,输入 3.0V,输出数字量是多少?

        D = 3.0 × 4096 ÷ 5 = 2457.6 → 取整 2457 = 999H

        校验:2457 × 5 ÷ 4096 = 2.9998V ✓(差 0.2mV,在 ±0.5 LSB 内)

【练习三:算输出】

    8 位 D/A,Vref = 5V,输入数字量 0A0H(=160),输出多少伏?

        Vout = 160 × 5 ÷ 256 = 3.125 V

【练习四:采样率够不够】

    要采集一个最高频率 1kHz 的信号,采样率至少要多少?

        fs ≥ 2 × 1000 = 2000 Hz = 2kHz

        实际工程上一般取 5~10 倍(即 5~10kHz),留足余量。

【练习五:总线周期】

    8086 在 8MHz 下,一个总线周期多少纳秒?

        T = 1/8MHz = 125ns
        总线周期 = 4 × 125 = 500ns

------------------------------------------------------------------------
  全书知识地图
------------------------------------------------------------------------

    第 1 篇   数制与编码            十六进制、补码、ASCII
    第 2 篇   8086 结构与寄存器      BIU/EU、14 个寄存器、FLAGS
    第 3 篇   存储器与分段          段×16 + 偏移 = 20 位物理地址
    第 4 篇   寻址方式              7 种寻址、EA 计算、ModR/M
    第 5 篇   数据传送与算术        MOV/ADD/CMP/MUL、标志位
    第 6 篇   逻辑移位与汇编程序    SHL/SAR/串操作、完整程序
    第 7 篇   存储器与地址译码      片选、74LS138、地址范围
    第 8 篇   I/O 与中断            两套地址空间、中断响应六步
    第 9 篇   8259A                 三个寄存器、四组 ICW、EOI
    第 10 篇  8255A                 两个控制字、D7 区分
    第 11 篇  8253/8251A            初值计算、先低后高
    第 12 篇  A/D、D/A 与总线       分辨率、采样定理、总线周期

------------------------------------------------------------------------
  三条主线串起全书
------------------------------------------------------------------------

    主线一:地址
        第 3 篇(分段)→ 第 4 篇(EA)→ 第 7 篇(译码)→ 第 8 篇(端口)
        "CPU 怎么找到它要访问的那个东西"

    主线二:数据通路
        第 2 篇(寄存器)→ 第 5、6 篇(指令)→ 第 10-12 篇(接口芯片)
        "数据怎么从 A 搬到 B,路上经过什么"

    主线三:时序与控制
        第 2 篇(流水线)→ 第 8 篇(中断时序)→ 第 9 篇(ICW/OCW)
        → 第 11 篇(定时)→ 第 12 篇(总线周期)
        "什么时候做什么事,谁等谁"

    ★ 这三条主线想清楚,整门课就不再是一堆芯片和指令,
      而是一个"CPU 怎么和外界配合完成一件事"的完整故事。

------------------------------------------------------------------------
  学完之后你应该能做到
------------------------------------------------------------------------

    ① 给一段汇编,能说出每个寄存器和标志位的变化
    ② 给一个地址(如 1234H:5678H),能算出物理地址
    ③ 给一颗存储芯片,能设计出译码电路并算出地址范围
    ④ 给一个中断场景,能写出中断服务程序骨架
    ⑤ 给 8255A/8253 的需求,能算出控制字和初值
    ⑥ 给 A/D 的位数和基准,能算出分辨率和数字量
    ⑦ 看到一个新芯片的手册,知道从"端口、控制字、时序"三处入手

    ★ 最后一条最重要 —— 芯片会过时,这套方法不会。


========================================================================
  自测:12 题检验这一篇
========================================================================

  1. 为什么需要 A/D 和 D/A?
  2. 1 LSB 怎么算?8 位 5V 时是多少?
  3. 8 位 D/A 的最大输出为什么不是 5V?
  4. 数字量 80H 送进 8 位 5V 的 D/A,输出多少伏?
  5. A/D 转换的三步是什么?各产生什么误差?
  6. 采样定理是什么?为什么必须满足?
  7. 量化误差是多少?能消除吗?
  8. 8 位 5V 的 A/D,输入 2.5V 输出什么数字量?
  9. ADC0809 有 8 个通道,怎么选择?同一时刻能转几个?
 10. ADC0809 的四步操作流程是什么?
 11. 8086 为什么要地址锁存器?ALE 是干什么的?
 12. 8086 的一个总线周期有几个 T?5MHz 时多少 ns?

------------------------------------------------------------------------
  参考答案
------------------------------------------------------------------------

  1. 真实世界的量(温度、声音)是连续模拟量,计算机只能处理离散数字量,
     所以需要 A/D(模拟→数字)和 D/A(数字→模拟)做翻译。
  2. 1 LSB = 满量程 ÷ 2^n。8 位 5V:5 ÷ 256 = 19.53 mV。
  3. 因为 8 位只能表示 0~255,最高是第 255 个台阶:
     255 × 5/256 = 4.98V。差的那 1 个 LSB 是量化造成的。
  4. 128 × 5 ÷ 256 = 2.5V。
  5. 采样、量化、编码。采样误差来自采样率不足(混叠);
     量化产生 ±0.5 LSB 的误差;编码本身不产生误差。
  6. fs ≥ 2 × fmax。因为采样太慢会让高频信号被误认成低频(混叠),
     而且混叠后无法恢复。
  7. ±0.5 LSB。原理上不能消除,只能提高位数减小(每加 1 位误差减半)。
  8. 2.5 × 256 ÷ 5 = 128 = 80H。
  9. 用 CBA 三根线选(000~111 对应 IN0~IN7)。
     同一时刻只能转换一个通道(内部是 8 选 1 的多路开关)。
 10. ① CBA 选通道;② START 正脉冲启动;
     ③ 等 EOC 变高(查询或中断);④ OE 有效,读数据。
 11. 因为 8086 的地址和数据复用同一组引脚(AD0~AD15)。
     ALE 在 T1 时有效,告诉外部"现在线上是地址,快锁存"。
 12. 4 个 T。5MHz 时 T = 200ns,总线周期 = 800ns。


========================================================================
  本篇最容易犯的 6 个错
========================================================================

  1. 以为 8 位 D/A 满量程就是 Vref
     最大是 255/256 × Vref = 4.98V(5V 时)。

  2. 分辨率当成精度
     分辨率由位数决定(理论值),精度受电阻/温漂影响(实际值)。

  3. 采样率取 2 倍就够
     理论下限是 2 倍,实际工程要取 5~10 倍留余量。

  4. 以为 ADC 的 8 个通道能同时转换
     内部是 8 选 1 开关,分时复用,一次只能转一个。

  5. 转换期间让 CPU 查询死等
     100μs 相当于 100 条指令,应该用中断或 DMA。

  6. 忘了地址锁存器
     8086 的地址和数据复用,不锁存地址,整个系统就乱了。


========================================================================
  打印提示
========================================================================

    单个 HTML 文件,Ctrl+P 直接打印或存 PDF。
    波形图与时序图已设置避免跨页断开,正文按 10pt 排版。