用 逻辑电路 实现一个 开平方 算法
这篇文章 的 起因 是 《小梦 在 民科吧 发了一个 用 四则运算 开平方 的 帖》 https://www.cnblogs.com/KSongKing/p/13296121.html 。
《小梦 在 民科吧 发了一个 用 四则运算 开平方 的 帖》 也 发到了 反相吧 《小梦 在 民科吧 发了一个 用 四则运算 开平方 的 帖》 https://tieba.baidu.com/p/6811112759 。
我在 帖 里的 12 楼 说了, 小梦 的 算法 简单小巧, 适合 用在 计算器 上, 我们可以 设计一个 硬件电路 来 实现 它 。
这个 算法 是, 设 b 为 被开方数, 任取一个 正数 a, 令 c = ( b - a ² ) / ( 2 a ) , 令 a = a + c , 重复若干次 这个过程, a 就 很接近 b 的 平方根 了 。
这个 算法 可以 称为 “小梦算法” 。
先画 一个 逻辑电路图 :
图 (1)
左边 的 a 、b 、c 、diff 、v1 、v2 、abs_diff 、max_diff 是 存储单元, 也就是 内存, 也就是 内存单元, 假设 每个 存储单元 是 64 位 的, 可以 存储 64 位 浮点数 。
右边 的 F1 、F2 、F3 、F4 、F5 、F6 是 控制单元, 具体 的 控制 和 运算 逻辑 就在 控制单元 里 实现 。
橙色线 和 橙色箭头 是 控制信号线路 和 信号传递方向, 蓝色线 是 数据线路, 一根 线 在 实际中 可能是 多位 的 。
绿色线 和 绿色箭头 也是 控制信号线路, 表示 和 橙色 不同 的 控制分支 。
F1 、F2 、F3 、F4 、F5 、F6 都 会有 数据线 和 相关的 存储单元 相连, 图中 用 蓝线 简略 的 表示, 并没有 画出 具体 的 连接 线路 。
我们 定义 : 有电压 为 1, 无电压 为 0 。
开始运算 时, 输入端 输入 一个 1 脉冲, 就可以 触发 电路 开始 进行 开平方 运算 。 注意 是 1 脉冲, 不是 持续 的 1 。
先介绍一下 存储单元,
a 存放 a, a 是 中间结果, 也是 最终结果
b 存放 b, 也就是 被开方数
v1 存放 a ²
diff 存放 b - a ²
v2 存放 2 * a
c 存放 diff / v2
abs_diff 存放 diff 的 绝对值
max_diff 存放 精度值, 当 b - a ² 的 绝对值 , 也就是 abs_diff 小于 max_diff 时, a 为 达到 精度 的 开方结果 , 可以输出 。
开始 运算 前, 先 把 被开方数 存到 b, 同时 任取一个 正数, 比如 1 , 存到 a 。
然后, 向 输入端 输入 一个 1 脉冲, F1 接收 到 1 脉冲 后 接通电路, 开始工作 。 F1 的 工作 是 发信号 给 运算器, 让 运算器 计算 a ², 运算器 计算 结束后, F1 把 计算结果 存放到 v1 , 同时 发出 一个 1 脉冲, 触发 F2 开始工作 。
运算器 在 这个 图 里 没有 画出来, 运算器 是 一个 公共部件, F1 、F2 、F3 、F4 、F5 、F6 都会去调用 。
F1 的 内部电路 会 在 下文 画出来, 里面 会 画出 F1 调用 运算器 的 电路 和 逻辑 。
F1 的 内部电路 如下 :
图 (2)
图 (3)
输入端 接收 到 1 脉冲, 这个 1 脉冲 会让 “让 寄存器 A 变成 写入状态” 电路 接通, 这个电路 会 向 寄存器 A 发出 信号, 告诉 寄存器 A 变成 写入状态,
同时, 输入端 的 1 脉冲 还会 让 “让 寄存器 B 变成 写入状态” 电路 接通, 这个电路 会 向 寄存器 B 发出 信号, 告诉 寄存器 B 变成 写入状态,
同时, 输入端 的 1 脉冲 会 触发 延时开关, 延时开关 在 一段时间 后 输出 一个 1 脉冲 , 这个 1 脉冲 会 接通 下一个 操作 的 电路 。
这样 就可以 在 一个 操作 完成后 触发 下一个 操作 执行 。
为什么要用 延时开关 呢 ? 是 为了 确保 上一个 操作 完成 后, 才 触发 下一个 操作 。 因为 电路 的 运行 需要时间, 每一段电路 运作 需要 的 时间 也 不完全相同, 所以 需要 延时开关 在 一段时间 后 发出 1 脉冲 触发 下一个 操作, 这段时间 应该 足够 完成 当前操作, 这样 来 确保 触发 下一个 操作时, 当前 操作 已经 完成 。
从 图上 可以看到, “让 寄存器 A 变成 写入状态” 和 “让 寄存器 B 变成 写入状态” 是 F1 的 第一个 步骤, 这 2 个 操作 是 同时执行 的, 也可以说是 并行 执行 的 。
第 1 个 步骤 有 一个 延时开关, 当 “让 寄存器 A 变成 写入状态” 和 “让 寄存器 B 变成 写入状态” 完成 后, 延时开关 发出 1 脉冲, 触发 下一个 步骤 。
第 2 个 步骤 包含 2 个 操作, “打开 a 和 寄存器 A 的 通路, 让 a 的 数据 写入 寄存器 A” 和 “打开 a 和 寄存器 B 的 通路, 让 a 的 数据 写入 寄存器 B” ,
这 2 个 操作 也是 同时执行 的, 第 2 个 步骤 也 有 一个 延时开关, 这 2 个 操作 完成 后, 延时开关 发出 1 脉冲, 触发 下一个 步骤 。
到 目前为止, 每一个 操作 是 一段 电路, 这一段 电路 在 输入端 输入 1 脉冲 时 工作, 1 脉冲 结束 后 电路 停止 。
1 脉冲 是 有电压, 这个 电压 使得 电路 接通 并 工作, 1 脉冲 结束 后, 无电压, 电路不工作 。 延时开关 被 触发 后, 即使 输入端 的 1 脉冲 结束, 也会 在 设定好 的 时间 后 在 输出端 发出 1 脉冲 。
当然, 我们需要 知道 每一个 步骤 完成 的 最大时间, 以此 来 设置 这个 步骤 的 延时开关 的 延迟时间, 延迟时间 应该 比 步骤 完成 的 最大时间 更大一点, 这样 有一点 冗余, 有利于 电路 的 稳定运行 。
图 (3) 的 第一个 操作 是 “向 运算器 发出 指令 计算 乘法”, 同时 会 触发 延时开关, 延时开关 发出 1 脉冲 应该是在 运算器 完成 运算 之后 。
也就是说, 延时开关 的 延迟时间 应该是 “向 运算器 发出 指令 计算 乘法” 电路 的 运行时间 + 运算器 计算乘法 的 时间 + 冗余时间 。
这需要 设计者 了解 运算器 的 运算时间 , 并以此 给 延时开关 设定 延迟时间 。
这个 设计 不算太好, 从 软件设计 的 角度来讲, 封装性 不太好, 高内聚 低耦合 不足 。 因为 这需要 运算器 将 运算时间 告知 其它 元件, 当 运算器 的 运算时间 发生变化, 与之相关 的 操作 的 延时开关 的 延迟时间 都要 修改 。
所以, 我们可以 把 图 (3) 的 这部分 设计 改一下 :
图 (4)
图 (4) 和 图 (3) 不同 的 地方 是 “向 运算器 发出 指令 计算 乘法” 的 下面 是 “一次性开关”, 图 (3) 中 此处 是 延时开关 。
运算器 计算 完成 时 会 通过 输出端 输出 1 脉冲, 表示 计算完成, 这个 1 脉冲 会 触发 外部电路 进行 下一步 操作 。
实际上 可以 不用 延时开关, 让 运算器 的 输出端 直接 连到 到 下一个 操作 的 电路, 运算器 计算完成 时 在 输出端 输出 1 脉冲 可以 直接 触发 下一个 操作 的 电路 。
为什么 这里 要用 一次性开关 呢 ?
因为 有 多个 元件 会 用到 运算器, 所以, 运算器 的 输出端 会 连接 到 多个 元件, 向 多个 元件 输出 完成信号 1 脉冲, 这就需要 区分 当前 调用 运算器 的 是 哪一个 元件, 运算器 输出 的 完成信号 应该 只 发给 当前 调用 运算器 的 元件 。
此时, 一次性开关 就 派上用场 了 。
一次性开关 是 这样 :
开始时, 一次性开关 处于 停止状态, 输出端 输出 0, 也就是 无电压 。
当 控制端 输入 1 脉冲 时, 触发 一次性开关, 进入 工作状态, 进入 工作状态 后, 输出端 仍然 是 0, 当 输入端 输入 1 脉冲 时, 输出端 输出 1 脉冲, 同时, 停止 一次性开关, 让 一次性开关 恢复 停止状态 。
在 停止状态 下, 无论 输入端 输入 1 还是 0, 输出端 的 输出 都是 0 。 或者说, 在 停止状态 下, 输入端 的 输入 无效 。
图 (1) 里 的 F6 “判断 abs_diff < max_diff” 会 2 次 用到 运算器, 一次 是 计算 diff 的 绝对值 (diff 的 绝对值 会 存到 abs_diff ), 一次 是 比较 abs_diff 和 max_diff 的 大小 。
运算器 除了 加减乘除, 还 可 求 绝对值 和 比较大小, 把 要 求 绝对值 的 数 放到 寄存器 A, 再 发指令 告知 运算器 求 绝对值, 运算器 会 对 寄存器 A 里 的 数 求 绝对值, 并把 结果 存放到 寄存器 C 。
运算器 提供 比较 大小 的 功能, 把 要 比较 大小 的 2 个 数 存到 寄存器 A 和 寄存器 B, 发指令 告知 运算器 比较大小, 运算器 会 比较 寄存器 A 和 寄存器 B 里 2 个 数, 运算器 会 提供 3 个 输出端 来 返回 比较结果, 这 3 个 输出端 是 “大于” 、“小于” 、“等于” , 若 A > B , 大于端 输出 1 脉冲, 若 A < B, 小于端 输出 1 脉冲, 若 A = B , 等于端 输出 1 脉冲 。
运算器 本身 是一个 复杂 的 部件, 基本原理 也是 上文 所讲 的 逻辑电路 原理 , 给出 适当 的 算法, 用 上文 的 逻辑电路 原理 可以 设计出 运算器 。
到 目前为止, 运算器 提供 加减乘除 、求绝对值 、比较大小, 一共 6 个 功能, 对应 6 个 指令, 可以用 3 位 指令 来 表示 。
0 和 1 的 3 位 组合 有 8 种, 000, 001, 010, 011, 100, 101, 110, 111 , 去掉 000 , 还有 7 种, 足够表示 6 个 指令 。
为什么 要 去掉 000 呢 ? 因为 000 表示 无指令, 空闲 。
6 个 指令 可以这样 表示 :
001 加
010 减
011 乘
100 除
101 求 绝对值
110 比较大小
运算器 提供 3 位 线路 作为 指令 输入端 就可以 。 也可以说, 运算器 的 指令 输入端 是 3 个 引脚 。 也可以说, 运算器 的 指令 输入端 是 3 条 接线 。
我以前写过一篇文章 《漫谈 计算机硬件 的 设计 和 实现》 https://www.cnblogs.com/KSongKing/p/9866334.html , 介绍过 “指令开关”, 指令开关 的 学名 似乎 叫 “译码器” 和 “真值表” 。
具体 的 电路设计, 比如 上文 涉及到 的 一些 开关元件 : 延时开关 、 一次性开关, 我会 另外写一篇 文章 《设计 逻辑电路 的 开关元件》 https://www.cnblogs.com/KSongKing/p/13412340.html 来 介绍 。
严格的说, 上文 说 的 “延时开关” , 应该是 “延时脉冲开关”, 延时开关 是 触发 后 延迟一段时间 后 导通, 导通 后 一直 保持 导通状态 。
延时脉冲开关 是 触发 后 延迟一段时间 后 输出 一个 脉冲, 脉冲 是 指 导通 一段时间 后 断开, 并非 一直 导通, 导通时间 就是 脉冲宽度 。
当然, 还有 一种 延时开关 是 触发 后 立即 导通, 延迟一段时间 后 断开 。