📖 名词溯源:为什么叫「进制」「基数」「位权」 「进制」就是「进位」的规则——满几进一。十进制是「满十进一」,所以基数是 10;二进制是「满二进一」,基数是 2。「位权」就是「这一位有多重」:个位权重 1、十位权重 10、百位权重 100……。三个词合起来,就完整描述了一套记数法。
🎯 本章学习目标
- 理解「位权」与「进位制」的本质,说清十进制为什么是十进制。
- 掌握二进制 ↔ 十进制 ↔ 八进制 ↔ 十六进制的转换方法。
- 从「电平稳定性」的物理层面,说清为什么计算机必须用二进制。
- 理解负数在计算机里如何表示——尤其是「补码」为什么被选中。
- 掌握小数的二进制表示(乘 2 取整),知道 0.1 为什么「装不进」二进制。
- 会用补码范围与溢出规则,判断一次 8 位运算的结果还能不能信。
2.1从 bit 到数:位权与进位制
一个 bit 只有两个状态:0 和 1。要表示更大的数,最简单的办法是把多个 bit 排成一排,让每个位置代表不同的「分量」。
在动手之前,先问一个你可能从没认真想过的问题:十进制「234」这三个数字,为什么代表二百三十四?
核心思想 任何一个进位制记数法,都由两个东西决定:基数(base)和位权(place value)。「234」中的 2 写在百位,它的意义不是「2」,而是「2 × 10²」;3 在十位,是「3 × 10¹」;4 在个位,是「4 × 10⁰」。把它们加起来:2×100 + 3×10 + 4×1 = 234。
这里的关键概念叫 位权:同一个数字,放在不同位置,权重不一样。从右往左,每一位的权重是基数(10)的 0 次方、1 次方、2 次方……依次递增。
把这个逻辑照搬到「满二进一」,就得到了二进制。二进制里只有 0 和 1 两个符号,但位权从右往左是 1、2、4、8、16……(也就是 2⁰、2¹、2²、2³……)。
| 位置(从右往左) | 第 1 位 | 第 2 位 | 第 3 位 | 第 4 位 | 第 5 位 |
|---|---|---|---|---|---|
| 十进制位权 | 10⁰ = 1 | 10¹ = 10 | 10² = 100 | 10³ = 1000 | 10⁴ = 10000 |
| 二进制位权 | 2⁰ = 1 | 2¹ = 2 | 2² = 4 | 2³ = 8 | 2⁴ = 16 |
所以,二进制数 1011₂(下标 2 表示这是二进制)的每一位拆开是:
| 数字 | 1 | 0 | 1 | 1 |
|---|---|---|---|---|
| 位权 | 2³ = 8 | 2² = 4 | 2¹ = 2 | 2⁰ = 1 |
| 贡献 | 1×8 = 8 | 0×4 = 0 | 1×2 = 2 | 1×1 = 1 |
加起来:8 + 0 + 2 + 1 = 11。所以 1011₂ = 11₁₀。
位权为什么「必然」是基数的幂?
上面这些例子看多了容易产生错觉:位权是 1、2、4、8「碰巧排成」2 的幂。其实恰恰相反——位权是幂,是「满几进一」这条规则的数学必然结果。推理只有三步:
第一,按进位规则,个位上每攒够 r 个 1,就要向十位进 1。也就是说,第二位的「1」,等于第一位的「r 个 1」——所以第二位的位权是 r¹。第二,同理,第二位攒够 r 个,向第三位进 1,第三位的位权就是 r × r¹ = r²。第三,以此类推,从右往左第 k 位的位权必然是 rk−1。位权表不是「规定」出来的,而是从进位规则里长出来的——这也解释了为什么不同进制之间可以无损互换:它们只是同一套计数逻辑穿了不同的衣服。
把这套推理写成通式,就得到本节最重要的一个公式。设基数为 r,各位数字从右往左为 d₀, d₁, d₂, …,那么:
这条通式的价值在于它对任何进制都成立:十进制、二进制、十六进制,甚至工程中偶尔遇到的五进制、六十进制(时间 60 秒 = 1 分,就是「满六十进一」的活化石)。掌握了通式,你不需要为每种进制单独背一套方法。
📐 例题 1 五进制数 324₅ 等于十进制多少?
某设备的拨码开关用五档旋钮(0–4)记录档位,读数「324」是按五进制写的。请把它换算成十进制,并回答:这个三位五进制数能表示的最大值是多少?
思路 直接套位权通式:基数 r = 5,位权从右往左是 5⁰、5¹、5²。最大值则是各位都取最大数字 4。
解 324₅ = 3×5² + 2×5¹ + 4×5⁰ = 3×25 + 2×5 + 4×1 = 75 + 10 + 4 = 89。最大值是 444₅ = 4×25 + 4×5 + 4 = 100 + 20 + 4 = 124(即 5³ − 1,比它再大 1 就得进位成四位数 1000₅ = 125)。
答:324₅ = 89₁₀;三位五进制最大 124₁₀。 ⚠ 易错点:别把「最大值」写成 4×5² = 100——后面两位也各自能取到 4;另一个规律值得记住:n 位 r 进制的最大值恒等于 rⁿ − 1,后面讲补码范围时会再用到它。
小数点右边:位权的负指数延伸
通式还能往右延伸。小数点右边第一位的位权是 r⁻¹,第二位是 r⁻²……例如十进制 0.25 = 2×10⁻¹ + 5×10⁻²;二进制 0.101₂ = 1×2⁻¹ + 0×2⁻² + 1×2⁻³ = 0.5 + 0.125 = 0.625。位权向右「越走越轻」的物理直觉很朴素:越靠右的位,表示「越细的零头」。这条延伸规则是下一节小数转换的全部依据。
辨析:「数」与「数的写法」是两回事
这里值得停一下,做一个初学时最容易忽略的辨析。「十一」这个数,与你写下 11、1011₂、B₁₆ 还是「拾壹」,没有本质关系——数是抽象的量,写法只是它的某种「编码」。同一个数在不同进制下写法不同,但大小完全相等:1011₂ = 11₁₀ = B₁₆。就像同一架飞机,叫它的型号编号、注册号还是口语昵称,飞机本身没变。
这个辨析在工程上有直接后果:数不会因为换了一种写法就改变大小,但「位数」会变。十进制的两位数 99,在二进制要写 7 位(1100011)。所以在设计设备接口时,「这个字段给几位」从来不是看十进制有几位,而是看它要覆盖的最大值需要多少位 2 的幂——第 1 章的 2ⁿ ≥ N 法则,在本章有了具体的执行工具。
🧠 一句话记忆 进制 = 满几进一;位权 = 这一位有多重。任何进制的数,都是「各位数字 × 位权」的总和。
2.2二进制与十进制:互相换算
二进制 → 十进制:按位权展开相加
这是最直接的方向:把每一位的「数字 × 位权」加起来即可。例如 1101₂:
1101₂ = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8 + 4 + 0 + 1 = 13₁₀
熟练之后可以跳过乘法,直接「挑 1 的位权相加」:1101 的第 3、2、0 位是 1,于是 8 + 4 + 1 = 13。位权从小到大是 1、2、4、8、16、32、64、128——这串数值得背到滚瓜烂熟,它就是后面反复出现的「8421 码」的骨架,也是你读任何二进制数据时的「乘法表」。
十进制 → 二进制:除 2 取余
反方向稍需技巧,但原理很朴素:不断地除以 2,记下每次的余数,最后把余数倒着读,就是二进制结果。为什么有效?因为「除以 2 的余数」正好暴露了这个数在二进制下的最低位。
底层原理 一个数除以 2,余数只能是 0 或 1,而它恰好等于二进制表示的最低位。除完一次,商再继续除 2,新的余数就是次低位……如此反复,直到商为 0。这就是「除 2 取余,逆序排列」。
再补一层「为什么」:把位权通式两边同除以 2——d₀·1 + d₁·2 + d₂·4 + … 除以 2 后,除了 d₀·2⁰ = d₀ 这一项除不动(余项),其余每一项的位权都是 2 的 1 次以上幂,整除。所以余数唯一暴露的就是 d₀,商里剩下的恰好是「去掉最低位」之后的那个数。除一次剥一位,像剥洋葱——「除 2 取余」因此必然正确。
📐 例题 2 把十进制 45 转成二进制,并验算
某传感器读数经滤波后得到十进制值 45,要以 6 位无符号二进制存入寄存器。写出转换过程与结果,并用按位权展开验算。
思路 45 大于 32、小于 64,6 位(位权 32,16,8,4,2,1)必然装得下。用除 2 取余逐位剥出,最后按位权展开回算验证。
解 连续除 2:45÷2=22 余 1;22÷2=11 余 0;11÷2=5 余 1;5÷2=2 余 1;2÷2=1 余 0;1÷2=0 余 1。余数从下往上读:101101₂。验算(按位权挑 1 相加):32 + 8 + 4 + 1 = 45 ✓。写成 6 位补齐格式即 101101。
答:45₁₀ = 101101₂。 ⚠ 易错点:余数必须「逆序」读——最后得到的余数是最高位;验算时容易漏加 2¹ 位(这里恰好是 0),建议把每个「1」所在的位权逐个圈出来再求和。
验算不必手算,一行代码就能「双保险」:
n = 45
print("45 的二进制:", bin(45), "| 八进制:", oct(45), "| 十六进制:", hex(45))
运行结果:45 的二进制: 0b101101 | 八进制: 0o55 | 十六进制: 0x2d——与手算 101101₂ 完全一致。0b / 0o / 0x 分别是 Python 里二、八、十六进制的前缀记号,这个记法在 C 语言与许多手册中也通用。
换一条思路:减权凑数法(8421 法)
除 2 取余是「机械正确」的方法,但工程现场更常用的其实是心算更快的减权法:盯着 1、2、4、8、16、32、64、128 这串位权,从最大的「装得下」的那位开始减。比如 45:先减 32 剩 13,再减 8 剩 5,再减 4 剩 1,最后减 1 剩 0——用掉的位权是 32、8、4、1,对应位写 1,其余写 0,直接得到 101101。两种方法互为校验:除 2 取余保证不出错,减权法练出手感。建议先熟练其一,再用另一方法交叉验证——工程里数字写错一位就是一次故障,双方法对拍是廉价而有效的保险。
小数怎么办:乘 2 取整
整数部分「除 2 取余」,小数部分恰好镜像过来:乘 2 取整。原理同样可以从位权通式看穿:小数部分 = d₋₁·2⁻¹ + d₋₂·2⁻² + …,两边同乘 2,d₋₁ 那一项变成 d₋₁·2⁰ = d₋₁,整个被顶到「≥1」的位置——所以乘 2 后的整数部分,唯一暴露的就是小数点后第一位 d₋₁;去掉它,剩下的小数部分继续乘 2,剥出第二位……也是剥洋葱,只是方向反过来。
📐 例题 3 把 0.625 转成二进制;0.1 为什么「永远转不完」?
(1)用乘 2 取整法把 0.625₁₀ 转成二进制;(2)同样方法处理 0.1,观察会发生什么。
思路 乘 2 取整:每步把小数部分乘 2,取走整数部分作为一位,再对剩下的小数部分重复,直到小数部分归零(或看出循环)。
解 (1)0.625×2 = 1.25,取 1,剩 0.25;0.25×2 = 0.5,取 0,剩 0.5;0.5×2 = 1.0,取 1,剩 0——结束。读数:0.101₂。验算:0.101₂ = 0.5 + 0.125 = 0.625 ✓。(2)0.1×2 = 0.2 取 0;0.2×2 = 0.4 取 0;0.4×2 = 0.8 取 0;0.8×2 = 1.6 取 1 剩 0.6;0.6×2 = 1.2 取 1 剩 0.2……从 0.2 开始进入循环,得 0.1₁₀ = 0.00011 0011 0011…₂,0011 无限重复,永远乘不到尽头。
答:0.625 = 0.101₂(3 位精确表示);0.1 在二进制下是无限循环小数 0.0(0011)…,位数再多也只能「约等」。 ⚠ 易错点:0.625 这类「分母只含因子 2」的小数(1/8、3/8、5/8…)才能精确表示;0.1、0.7 这类分母含因子 5 的小数必然循环——这不是计算失误,是数学上的「不可精确」,下一小节马上给出工程后果。
浮点数预告:0.1 + 0.2 ≠ 0.3 的伏笔
「0.1 转不完」直接预言了一个著名现象:几乎所有编程语言里,0.1 + 0.2 的输出都不是 0.3,而是 0.30000000000000004。原因就在于:计算机存小数时只给它固定的有限位数(通常是 64 位,遵循 IEEE 754 标准),而 0.1 的二进制展开无限长,只能被「拦腰截断」——截断后存的其实是比 0.1 略大或略小的另一个数,两个带误差的数相加,误差就暴露在输出里。
这不是 bug,而是「二进制小数 + 有限位数」这对约束的必然代价。工程上因此有一条不成文的铁则:涉及钱、涉及告警门限、涉及安全判据的比较,永远不要用浮点数做「相等」判断,而是判断「差值是否小于一个约定容差」。完整展开 IEEE 754 的符号位、指数、尾数需要一些铺垫,本书会在内存一章(讲数据存储时)回来兑现这个承诺;此刻你只需要记住:整数在二进制里是精确的,小数是「租来的精度」。
📐 例题 4 综合转换:十进制 202.375 → 二进制
把 202.375₁₀ 转换成二进制,要求整数、小数两部分分别用规范方法完成,并对最终结果做反向验算。
思路 整数部分与小数部分的「进位方向」相反,所以算法也相反:整数用除 2 取余(或减权法),小数用乘 2 取整,最后用小数点拼起来。
解 整数 202:减权法——202 减 128 剩 74,减 64 剩 10,减 8 剩 2,减 2 剩 0,用掉位权 128、64、8、2 → 11001010₂。小数 0.375:0.375×2=0.75 取 0;0.75×2=1.5 取 1 剩 0.5;0.5×2=1.0 取 1 → 0.011₂。拼接:202.375₁₀ = 11001010.011₂。反向验算:128+64+8+2 = 202 ✓;0.25+0.125 = 0.375 ✓。
答:202.375₁₀ = 11001010.011₂。 ⚠ 易错点:整数、小数两段的「读数方向」相反——除 2 取余的余数从下往上读,乘 2 取整的整数从上往下读,初学时最容易把小数部分也倒过来读;另外小数验算别把 0.011 错算成 0.375 之外的值(0.25+0.125,两个位权一个都不能漏)。
🧠 一句口诀 十转二,除 2 取余,倒着读;二转十,按位权,加起来。整数除 2 取余,小数乘 2 取整——两段方法互为镜像。
2.3八进制与十六进制:二进制的「速记」
二进制有一个大毛病:太长了。一个十进制数 255,写成二进制是 11111111——8 位。人看着眼晕,也容易抄错。于是人们发明了「速记法」:把二进制每几位打包成一个符号。
打包的关键在于——进制之间必须是 2 的幂次关系,才能直接分组互换,不用算。
- 八进制(octal,基数为 8 = 2³):每 3 位二进制打包成 1 位八进制(0–7)。
- 十六进制(hexadecimal,基数为 16 = 2⁴):每 4 位二进制打包成 1 位十六进制(0–9,再加 A–F 表示 10–15)。
📖 名词溯源:octal 与 hexadecimal octal 源自拉丁语 octo(八);hexadecimal 是 hexa(希腊语「六」)+ decimal(十)= 十六。十六进制里需要 16 个符号,阿拉伯数字只有 10 个,于是借用字母 A、B、C、D、E、F 来补上 10、11、12、13、14、15。
为什么是 3 位一组、4 位一组?因为 2³ = 8、2⁴ = 16:每组二进制恰好能完整覆盖新进制的一位,不多不少。看一个例子,二进制 1101 0101₂ 转十六进制:
| 十进制 | 二进制 | 八进制 | 十六进制 |
|---|---|---|---|
| 0 | 0000 | 0 | 0 |
| 1 | 0001 | 1 | 1 |
| 2 | 0010 | 2 | 2 |
| 7 | 0111 | 7 | 7 |
| 8 | 1000 | 10 | 8 |
| 10 | 1010 | 12 | A |
| 13 | 1101 | 15 | D |
| 15 | 1111 | 17 | F |
你现在可能觉得十六进制很怪,但它无处不在:内存地址、颜色代码(比如网页里的 #0f766e 就是十六进制)、错误码里都藏着十六进制。等讲到内存和机器语言时,你会感谢它帮人类「压缩」了二进制。
十六进制 ↔ 十进制的双向直算
分组速记只解决「二进制 ↔ 8/16 进制」;十六进制和十进制之间没有幂次捷径,仍要老老实实用位权通式。好在方向不同、方法对称,值得各练一遍形成手感:
📐 例题 5 十六进制 2AF₁₆ = ? ₁₀;十进制 500 = ? ₁₆;八进制 345₈ = ? ₁₀
(1)按位权展开求 2AF₁₆ 的十进制值;(2)把 500₁₀ 转成十六进制(除 16 取余);(3)按位权展开求 345₈ 的十进制值。
思路 十六进制的位权从右往左是 16⁰、16¹、16²;字母先翻译回数字(A=10,F=15)。十转十六用「除 16 取余」,与十转二同构。
解 (1)2AF₁₆ = 2×16² + 10×16¹ + 15×16⁰ = 2×256 + 160 + 15 = 512 + 160 + 15 = 687。(2)500÷16 = 31 余 4;31÷16 = 1 余 15(F);1÷16 = 0 余 1。余数从下往上读:1F4₁₆。验算 1×256 + 15×16 + 4 = 256 + 240 + 4 = 500 ✓。(3)345₈ = 3×8² + 4×8¹ + 5×8⁰ = 192 + 32 + 5 = 229。
答:2AF₁₆ = 687₁₀;500₁₀ = 1F4₁₆;345₈ = 229₁₀。 ⚠ 易错点:十六进制字母必须先换成数字再乘位权,直接「乘 A」就错了;除 16 取余时余数 15 要写成 F,不是「15」两个字符——一位十六进制只装一个符号。
八进制和十六进制之间怎么转?——借二进制当「中转站」
八进制与十六进制之间没有直接的幂次关系(8 与 16 不是彼此的幂),硬算要走十进制绕路。但工程上有个标准动作:两者都先转成二进制,再按目标进制分组——二进制是它们的「公共母语」。以 2AF₁₆ 转八进制为例:先把每位十六进制摊成 4 位二进制,得 0010 1010 1111₂;再从右往左每 3 位重分组(10 101 011 11 → 补齐 010 101 011 111),读出 1257₈。位权验证:1×512 + 2×64 + 5×8 + 7 = 512+128+40+7 = 687 ✓,与例题 5 的结果严丝合缝。这种「经二进制中转」的思路在工程数据手册里天天用:看到 8/16 进制,脑内先摊平成二进制,再按需要重新打包。
工程现场:这些进制分别守在哪些岗位上
三种进制在工程文档里的「势力范围」相当稳定,提前知道能少走弯路:
| 进制 | 典型岗位 | 识别记号 | 为什么是它 |
|---|---|---|---|
| 二进制 | 位定义表(bit0–bit7 每一位的功能)、寄存器说明 | 0b 前缀、下标 ₂、成组书写 0100 0001 | 一位一义,直接对应物理开关 |
| 八进制 | 机载数据总线(如 ARINC 429)的标号字段、Unix 文件权限 | 0o/0 前缀、下标 ₈ | 历史习惯 + 3 位一组的字节内分组 |
| 十六进制 | 内存地址、MAC 地址、颜色码、错误码、报文 dump | 0x 前缀、下标 ₁₆ | 4 位一组恰好对齐 8 位字节(1 字节 = 2 个十六进制符号) |
特别说一句八进制这个「冷门选手」:航空电子的经典数据总线标准 ARINC 429,给每条数据打的「标号」(label)就是按八进制书写的。比如手册里写「标号 205」,指的不是十进制的 205,而是 205₈ = 2×64 + 0 + 5 = 133₁₀,对应二进制 1000 0101。曾有工程师把它当十进制读,对表时怎么都对不上号——这类「进制陷阱」正是八进制没有退出历史舞台的原因之一:它还守在老标准里,你认不出它,它就能坑你。另一个出名的历史坑是编程语言 C:一个数字字面量前缀 0 表示八进制,把 011 写进代码得到的是 9 而不是 11。
📖 一个流传已久的程序员笑话 「为什么程序员分不清万圣节(Halloween,10 月 31 日)和圣诞节(Christmas,12 月 25 日)?因为 OCT 31 == DEC 25。」OCT 是 October(十月)也是 octal(八进制),DEC 是 December(十二月)也是 decimal(十进制):八进制的 31 = 3×8 + 1 = 25,恰好等于十进制的 25。一个笑话把「进制只是数的不同写法」这个道理讲透了。
🧠 一句话记忆 八进制 = 3 位一组,十六进制 = 4 位一组。2 的幂次进制之间,分组即换算,无需计算;八↔十六之间,借二进制中转。
2.4为什么必须用二进制:回到电平
现在回到第一章留下的那个问题,把它说透:为什么人类用十进制,计算机却非要用二进制?
答案不在数学,而在物理。计算机用「电压」来承载信息,而电压是一个连续、会漂移、会被干扰的物理量。
电压的「噪声」问题
真实电路里,电压从来不是精确的。电源会波动、邻近的导线会串扰、温度会改变元件的特性。如果你规定「1 伏代表数字 1,2 伏代表数字 2,……9 伏代表数字 9」,那么只要电压飘一点点——比如从 2.0 伏飘到 2.4 伏——这个数字到底是 2 还是 3 就分不清了。
底层原理:状态数越少,容错区间越大 假设信号范围是 0–5 伏。十个状态(0–9)时,每个状态只分到约 0.5 伏的区间,稍有噪声就出错;两个状态(0 和 1)时,每个状态分到约 2.5 伏的区间,即使电压飘 1 伏,只要没跨过中间线,依然能准确判定是 0 还是 1。这就是为什么二进制最抗干扰、最可靠。
而且,二进制不仅更可靠,还让运算电路更简单——因为「0/1」天然对应「通/断」「高/低」「有/无」,这些正是晶体管开关最擅长表达的东西(下一章讲)。数学上的简洁,恰好与物理上的可实现性合拍,这才是二进制胜出的根本原因。
把「容错」变成可计算的数字:噪声容限
「区间宽」只是定性说法,工程上要把它量化成一个可查、可算的参数,叫噪声容限(noise margin)。定义只涉及四个电压:
- VOH:输出端代表「1」时,保证给出的最低电压(输出高电平下限);
- VIH:输入端判定「1」所要求的最低电压(判决上门槛);
- VIL:输入端仍判「0」所允许的最高电压(判决下门槛);
- VOL:输出端代表「0」时,保证给出的最高电压(输出低电平上限)。
直觉解读:发送方「保证至少给这么多」,接收方「只要求至少给那么多」,两者之间的富余量,就是留给噪声糟蹋的空间。富余越大,越不怕干扰。下面这道例题把图 2-3 的定性结论变成一次真正的工程计算。
📐 例题 6 CMOS 电平的噪声容限与十状态方案对比
某 5 V 供电的 CMOS 数字电路,手册给定:VOH = 4.5 V,VOL = 0.5 V,VIH = 3.5 V,VIL = 1.5 V。(1)求高、低电平的噪声容限;(2)若改用「0–5 V 均分十个电平表示 0–9」的方案,每个状态的判决区间只有约 0.5 V,比较两种方案抗噪声能力的差距。
思路 直接代入噪声容限公式;对比时用「容限 ÷ 区间宽度」看同样的噪声扰动分别处在什么位置。
解 (1)NMH = 4.5 − 3.5 = 1.0 V;NML = 1.5 − 0.5 = 1.0 V。(2)二状态方案每个方向的容限都是 1.0 V;十状态方案每个状态的判决区间约 5 V ÷ 10 = 0.5 V,噪声一旦超过 0.25 V(半个区间)就可能跨过判决边界判错。两者相差 1.0 ÷ 0.25 = 4 倍。也就是说:同样 0.8 V 的电源纹波,二进制稳如泰山(1.0 V 容限内),十状态方案早已全军覆没。
答:高、低电平噪声容限各 1.0 V;抗噪安全余量约为十状态方案的 4 倍。 ⚠ 易错点:容限不是「整个区间宽 2.5 V」,那是不考虑输入输出规格差异的粗估;严格的容限必须用「输出保证值 − 输入要求值」,它总比理论区间小——留出的是器件参数分散性的余地。
工程现场:航空设备的「离散量」为什么也是二进制
这套「宽区间判决」的思路不只活在芯片内部。机载设备的信号清单里有一大类叫离散量(discrete signal)——本质上就是「电路级别的 0/1」:某舱门关好与否、某拨动电门在不在「接通」位、某继电器吸合与否。工程惯例恰恰不挑「精确电压」,而是约定非常宽的两档,例如「地/开(GND/Open)」或「28 V 供电 / 地」两种状态,中间留出巨大的判定缓冲。
为什么这么设计?因为机载环境是电磁干扰的重灾区:电机启动、继电器通断、变频器工作都会在信号线上激起数伏的干扰脉冲。离散量用「几十伏量级的大台阶」传一个 bit,几十伏的信号摆幅对抗几伏的干扰,就是图 2-3「宽区间」思想放大到整机层面的应用。从晶体管到整机,二进制抗噪的哲学一以贯之:状态少、台阶大、判决稳。等你读到故障排查手册里「离散量有效/无效的电压门限定义」,应当能认出:那正是噪声容限公式 VIL、VIH 的工程化身。
📖 历史注脚:不是没人试过别的进制 三进制在数学上被证明是「每位信息量最优」的基数(e ≈ 2.718 最接近的整数),1958 年苏联确实造过以三进制(平衡三进制:−1/0/1)工作的计算机「Сетунь」,性能不俗。但它最终没有存活——根本原因还是物理:要做出稳定、廉价、快速的三状态元件,远比做一个「通/断」开关困难。数学最优 ≠ 工程最优,二进制的胜利是物理可实现性的胜利。
还有一个层面值得点破:1937 年香农在硕士论文里证明了布尔代数(只有真/假两种值的逻辑体系)可以用开关电路完整实现——从此「二进制」不再只是一种记数法,而是数学(逻辑)、电路(开关)、信息(bit)三者的公共交集。下一章讲逻辑门、后面讲指令系统时,你会看到这三条线一直拧在一起,而它们的接头就在「0 和 1」这里。
🧠 一句话记忆 二进制不是「更数学」,而是「更物理」:两个宽电压区间最抗干扰,也最好用开关实现。噪声容限 = 输出保证值 − 输入要求值。
2.5负数与补码:让减法变成加法
数字有正有负。二进制只有 0 和 1,没有「−」号。怎么表示负数?直觉做法是:拿出最高位当「符号位」,0 表示正、1 表示负。这叫「原码」,看起来顺理成章,但它有一个致命缺陷,我们一步步来看。
原码的困境
以 8 位为例,用原码表示 +3 是 0000 0011,−3 是 1000 0011。现在做 3 + (−3):
0000 0011 + 1000 0011 = 1000 0110 = −6(原码)❌ 结果错!
加法和减法用原码算,会得到错误结果。这意味着电路必须单独设计一套减法器,还得先判断符号、比较大小——复杂且浪费。
原码的毛病不止这一处,把它和历史上出现过的几种方案放在一起看,问题更清楚。8 位一共 256 个码点,理想情况下「表示 −128 到 +127」各占一半才叫物尽其用,但原码和反码都做不到:
| 方案 | −3 怎么写 | 0 的写法 | 加法器能直接算吗 | 范围 |
|---|---|---|---|---|
| 原码(符号位+绝对值) | 1000 0011 | 两个:0000 0000 和 1000 0000(+0 与 −0) | 不能,需先判符号再决定加减 | −127 ~ +127 |
| 反码(除符号位取反) | 1111 1100 | 两个:0000 0000 和 1111 1111 | 部分能,结果常差 1(要「循环进位」修补) | −127 ~ +127 |
| 补码(取反加一) | 1111 1101 | 只有一个:0000 0000 | 能,符号位当普通位一起加 | −128 ~ +127 |
这张表里藏着补码胜出的全部理由:0 只有一种写法(判断「等不等于 0」这一步在电路里极其高频,两种 0 是灾难);加减法共用一套电路;还白捡一个 −128(多出来的码点 1000 0000 在原码方案里是浪费的「−0」,在补码方案里成为一个真实的负数)。
补码:一个天才的「绕圈」思想
聪明人想到:如果负数能「伪装成」一个正数,让「减一个数」等于「加另一个数」,那么一台加法器就能同时做加法和减法,电路大幅简化。
这背后是一个关于「溢出回绕」的深刻思想。想象一个只能显示 0–999 的机械里程表(3 位十进制计数器):
把这个思想搬进 8 位二进制:8 位只能表示 0–255,超出就回绕(模 256)。那么「−1」就可以表示成「256 − 1 = 255」,即 1111 1111。于是 3 + (−1) 变成:
0000 0011 + 1111 1111 = 1 0000 0010(超出的最高位被丢弃)→ 0000 0010 = 2 ✓
结果正确!这就是 补码(two's complement):负数的表示,是「从 2ⁿ 里减去它的绝对值」。
深化:补码为什么「天然正确」——模与同余
上面只演示了「碰巧对」,其实补码的正确性有严格的数学保证。设寄存器宽 n 位,它能区分的码点共 2ⁿ 个,任何运算结果超出 2ⁿ 就回绕——数学上称这是一个模 2ⁿ 的世界。在模运算里,两个数「差 2ⁿ 的整数倍」就完全等价,记作 a ≡ b (mod 2ⁿ),读作「a 与 b 模 2ⁿ 同余」。于是 −1 与 255 同余(255 − (−1) = 256 = 2⁸),−3 与 253 同余,一般地:−x 与 2ⁿ − x 同余。补码做的事,就是给每个负数披上一件「与它同余的正数」的外衣:
为什么披上外衣之后加法就自动正确?因为同余关系对加法保持:若 a ≡ a′、b ≡ b′(mod 2ⁿ),则 a + b ≡ a′ + b′(mod 2ⁿ)。也就是说,用补码算出来的答案,与「真值直接相加」的结果永远只差 2ⁿ 的整数倍——而寄存器回绕恰好把 2ⁿ 的整数倍全部丢弃。两个机制严丝合缝:模运算保证数学上等价,寄存器回绕保证物理上兑现。这不是巧合设计,是把「硬件的局限」(位数有限)反转成「算法的武器」(同余替换)的思想典范。
补码圆环是这套机制最好的可视化——8 位的 256 个码点首尾相接成一个环,非负数住右半圈,负数住左半圈,−1 紧贴着 0:
求补码的快捷方法
真要每次算「2⁸ − 绝对值」太麻烦。有个三步口诀:取反,再加 1。例如求 −3(8 位):
- 写出 +3 的二进制:0000 0011
- 每一位取反(0↔1):1111 1100
- 加 1:1111 1101 ← 这就是 −3 的补码
为什么「取反再加 1」等价于「2ⁿ 减原数」? n 位全 1 的数(1111…1)正好等于 2ⁿ − 1。「取反」就是把某数从「全 1」里减掉,即 (2ⁿ − 1) − x;再加 1,就得到 2ⁿ − x。所以取反加 1 = 补码,分毫不差。
📐 例题 7 求 −100 的 8 位补码,并用补码加法验证 −100 + 60 = −40
(1)用「取反加一」写出 −100 的 8 位补码;(2)把 60 也写成 8 位补码,两者按二进制加法相加(最高位进位丢弃),验证结果等于 −40 的补码。
思路 先求 |−100| = 100 的 8 位二进制,取反加一得 −100 的补码;再做补码加法。加完后最高位是 1 就按补码规则读回真值。
解 (1)100 = 64 + 32 + 4 → 0110 0100;取反 → 1001 1011;加 1 → 1001 1100(用定义验证:256 − 100 = 156,而 1001 1100₂ = 128 + 16 + 8 + 4 = 156 ✓)。(2)60 = 32 + 16 + 8 + 4 → 0011 1100。相加:1001 1100 + 0011 1100 = 1101 1000(最高位是 1,读回:256 − 216 = 40,即 −40)。✓ 补码加法的结果与十进制真值 −100 + 60 = −40 完全一致。
答:−100 的补码是 1001 1100;1001 1100 + 0011 1100 = 1101 1000,即 −40 ✓。 ⚠ 易错点:读回结果时容易把 1101 1000 当无符号数 216 看——最高位为 1 的补码必须减 256 还原成负数;另外「取反加一」是对绝对值 100 做,不是对 −100 的符号位单独处理。
把上面这套「8 位补码加法」写成代码,用计算机替我们验证(& 0xFF 表示只保留低 8 位,模拟寄存器丢进位):
def add8(a, b):
r = (a + b) & 0xFF # 只留低 8 位 = 丢掉超出宽度的进位(回绕)
if r >= 128: r -= 256 # 最高位为 1 -> 补码世界里是负数,减 256 还原
return r
print("3 + (-3) =", add8(3, -3))
print("-100 + 60 =", add8(-100, 60))
print("127 + 1 =", add8(127, 1), " <- 溢出!结果跑到了 -128")
运行结果:3 + (-3) = 0 -100 + 60 = -40 127 + 1 = -128 (溢出)。前两行验证了补码加法的正确性;第三行暴露了一个新问题——溢出,马上专门处理它。
补码的势力范围:−128 ~ +127 从哪来
8 位共 256 个码点。按补码约定,最高位为 0 的 128 个码点(0000 0000 ~ 0111 1111)表示 0 ~ +127;最高位为 1 的 128 个码点(1000 0000 ~ 1111 1111)表示 −128 ~ −1。于是 n 位补码能表示的范围有一个干脆的公式:
注意这个范围不对称:负数比正数多一个(−128 有位置,+128 没有)。原因在图 2-5 的圆环上一目了然:0 占了一个码点,它算在「非负」半边,所以正数这边只剩 127 个位置,而负数那边满打满算 128 个。这不是缺陷,恰是补码「0 唯一」的代价与福利——比原码白白浪费一个「−0」划算得多。
| 二进制 | 无符号值 | 有符号值(补码) |
|---|---|---|
| 0000 0000 | 0 | 0 |
| 0000 0001 | 1 | +1 |
| 0111 1111 | 127 | +127 |
| 1000 0000 | 128 | −128 |
| 1111 1111 | 255 | −1 |
补码带来的好处是革命性的:加法和减法共用同一套电路,符号位也当作普通位一起参与运算,不用单独判断正负。这是后面第 5 章「减法器其实不用单独做」的伏笔。
溢出:补码世界的「越界警报」
圆环虽好,但有一件事它救不了你:结果超出范围时,补码不会报错,只会安静地给出一个错误的数。127 + 1 = −128,就是「顺时针多走一步、跨过 +127 与 −128 的交界、直接走进负数区」的圆环事故。这个现象叫溢出(overflow),判断它有一条几乎不用计算的规则:
溢出判据(符号位观察法) 只有同号相加才可能溢出(异号相加的结果绝对值变小,永远安全)。规则一句话:正 + 正 = 负,或负 + 负 = 正,必溢出;同号相加结果仍同号,则不溢出。电路里的等价实现是看「符号位的进位输入」与「符号位的进位输出」是否不同,硬件上只是一个异或门的事。
📐 例题 8 8 位补码运算:哪几题溢出?
用 8 位补码完成下列运算,先手工逐位相加,再判断是否溢出:(a)127 + 1;(b)−128 − 1;(c)−100 + 60;(d)−100 + (−60)。
思路 全部转补码相加,最高位进位丢弃;然后用「同号得异号」判据检查溢出。
解 (a)0111 1111 + 0000 0001 = 1000 0000 = −128。正 + 正 = 负 → 溢出(真值 128 装不下)。(b)−128 = 1000 0000,−1 = 1111 1111,相加 = 1 0111 1111 → 丢最高位 = 0111 1111 = +127。负 + 负 = 正 → 溢出(真值 −129 装不下)。(c)例题 7 已算:1101 1000 = −40。负 + 正,不会溢出 ✓。(d)−100 = 1001 1100,−60 = 1100 0100,相加 = 1 0110 0000 → 丢进位 = 0110 0000 = +96。负 + 负 = 正 → 溢出(真值 −160 超出 −128)。
答:(a)(b)(d)溢出,(c)安全。 ⚠ 易错点:(b)的丢进位后 0111 1111 表面看是「+127,挺正常」,恰恰是最阴险的一类溢出——结果数值大小尚可,符号却反了;只有用「同号得异号」的判据才能识破,肉眼看数值大小反而会被骗。
📐 例题 9 选位宽:飞行姿态角度量该用几位?
某数据接口要传输姿态俯仰角,量程约定 −90° ~ +90°,以 0.01° 为最小刻度(即存整数化的「百分度」,−9000 ~ +9000)。备选位宽:8 位、16 位。(1)8 位补码够不够?(2)16 位补码的余量有多大,用十六进制写出 +9000 和 −9000 的编码。
思路 把量程上界与各宽度的补码范围比较;写编码时按「取反加一」求负数。
解 (1)8 位补码范围 −128 ~ +127,而需要 +9000,差两个数量级,不够。(2)16 位补码范围 −32768 ~ +32767,覆盖 −9000 ~ +9000 后单边仍余 32767 − 9000 = 23767 个码点,余量充足。编码 +9000:按 16 位位权「减权法」——9000 = 2×4096 + 3×256 + 2×16 + 8×1,所以 +9000 = 2328₁₆(验算 8192 + 768 + 32 + 8 = 9000 ✓)。编码 −9000:取反加一——2328₁₆ 逐位取反得 DCD7₁₆,再加 1 得 DCD8₁₆;按定义复核 65536 − 9000 = 56536 = DCD8₁₆ ✓。
答:8 位不够(+127 < +9000);16 位够且单边余 23767 码点;+9000 编码 2328₁₆,−9000 编码 DCD8₁₆。 ⚠ 易错点:负数编码验算别忘「65536 − 9000」这条定义式,它与取反加一必然一致,两法对拍能防止十六进制手算出错;另外「0.01° 刻度」先整数化再比范围,别拿 0.01 去和补码范围硬比——补码装的是整数。
这道题展示的是工程接口设计里每天都在发生的思维过程:先约定物理量与刻度 → 换算成整数范围 → 选位宽 → 写正负两个端点的编码做验证。而「16 位角度量」在航电数据字定义中是真实存在的常见规格,你现在已经有能力独立核对其正负端编码是否正确——第 1 章说「信息 = 数据 + 解释规则」,这里补上了「规则」里最硬核的一块:数值字段到底怎么编码。
最后留一个衔接:补码把整数的世界安顿好了,但「小数」呢?乘 2 取整一节已经预告——小数在二进制里常常无限循环,只能按固定格式近似。完整的解决方案(IEEE 754 浮点数:符号位 + 指数 + 尾数)会在讲数据存储的章节展开。到那时你会发现,浮点数其实是「把一个数写成 ±1.x × 2^y 再编码 x 和 y」,位权思想仍然是一切的底座。
🧠 一句话记忆 补码 = 取反加一。它让「减」变成「加」,让一台加法器通吃加减法。范围 −2ⁿ⁻¹ ~ 2ⁿ⁻¹−1;正 + 正 = 负(或负 + 负 = 正)必溢出。
本章小结
- 任何进制的数都是「各位数字 × 位权」的总和;进制由基数(满几进一)决定,位权必然是基数的幂。
- 二进制 ↔ 十进制:按位权展开相加;反向用「除 2 取余,逆序排列」;小数部分镜像地用「乘 2 取整」,但只有分母只含因子 2 的小数能精确表示。
- 八进制(3 位一组)、十六进制(4 位一组)是二进制的「速记」,因为它们是 2 的幂次;八 ↔ 十六之间借二进制中转。
- 计算机用二进制是物理原因:两个宽电压区间最抗噪声(噪声容限可量化为 VOH − VIH 与 VIL − VOL),也最契合开关。
- 补码(取反加一)让负数「绕圈」成正数,使减法归入加法,简化电路;其数学本质是模 2ⁿ 的同余替换。
- n 位补码范围 −2n−1 ~ 2n−1 − 1;同号相加得异号即溢出,工程上必须在接口定义时留足位宽余量。
读完本章,你应该能……
- 说出「基数」「位权」的含义,并解释 234 为什么是二百三十四。
- 独立完成 1011₂ ↔ 11₁₀ 的往返换算。
- 把 11010101₂ 快速转成八进制和十六进制。
- 从电压噪声的角度解释「为什么不用十进制」,并说出噪声容限的四个电压定义。
- 用「取反加一」求出 −3 的 8 位补码,并验证 3 + (−3) = 0。
- 用乘 2 取整把 0.625 转成二进制,并解释 0.1 为什么转不完。
- 写出 8 位 / 16 位补码的范围,并用「同号得异号」判据检查 127 + 1 为什么溢出。
思考题
1. 十进制 255 用二进制、八进制、十六进制分别怎么表示?
提示:255 = 128+64+32+16+8+4+2+1 = 1111 1111₂;每 3 位一组是 11 111 111₂ = 377₈;每 4 位一组是 1111 1111₂ = FF₁₆。
2. 为什么说「二进制最抗干扰」?请从电压区间宽度来解释。
提示:状态越少,每个状态分到的电压区间越宽,噪声越难跨越判决阈值。可以用噪声容限公式定量:NMH = VOH − VIH,NML = VIL − VOL。
3. 「除 2 取余」为什么能得到二进制?余数和最低位有什么关系?
提示:除以 2 的余数(0 或 1)恰好是二进制的最低位;商继续除,余数依次是更高位。位权通式两边除以 2,只有最低位项除不动,余数唯一暴露 d₀。
4. 补码的「取反加一」为什么等价于「2ⁿ 减原数」?
提示:全 1 = 2ⁿ−1;取反 = (2ⁿ−1)−x;再加 1 = 2ⁿ−x。
5. 把十进制 100.8125 转成二进制(整数、小数都要转)。
提示:整数 100 = 64+32+4 → 1100100₂;小数 0.8125×2 = 1.625 取 1,0.625×2 = 1.25 取 1,0.25×2 = 0.5 取 0,0.5×2 = 1.0 取 1 → 0.1101₂。合起来 1100100.1101₂。验算:0.5+0.25+0.0625 = 0.8125 ✓。
6. 8 位补码运算 90 + 60,先判断会不会溢出,再给出寄存器里的结果。
提示:90 = 0101 1010,60 = 0011 1100,两正数相加 = 1001 0110。正 + 正 = 负 → 溢出!寄存器结果 1001 0110 按补码读回 = −106,而真值是 150(超出 +127,装不下)。
7. 为什么机载数据总线的标号(如 ARINC 429 的「205」)要特别提醒「按八进制读」?如果错当十进制读,会发生什么?
提示:标号 205₈ = 2×64 + 0 + 5 = 133₁₀,二进制 1000 0101;错当十进制 205 读,则对应的二进制应为 1100 1101,按位定义表查询时会把数据字解释成完全另一类参数,对表必然对不上。