這篇文章 的 起因 是 《小夢 在 民科吧 發了一個 用 四則運算 開平方 的 帖》 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 來 介紹 。
嚴格的說, 上文 說 的 “延時開關” , 應該是 “延時脈沖開關”, 延時開關 是 觸發 后 延遲一段時間 后 導通, 導通 后 一直 保持 導通狀態 。
延時脈沖開關 是 觸發 后 延遲一段時間 后 輸出 一個 脈沖, 脈沖 是 指 導通 一段時間 后 斷開, 並非 一直 導通, 導通時間 就是 脈沖寬度 。
當然, 還有 一種 延時開關 是 觸發 后 立即 導通, 延遲一段時間 后 斷開 。