ハードウェアシミュレータその13
ここまででそれとなく動作するCPUぽい何かはできたのだが、機能を追加していくには8bit固定長の命令では幅が足りない。幅を増やして更なる独自命令セットを作るぐらいなら既存の何かにチャレンジすればいいのではないか、など考えてみて、そもそも既存の何かを作るには足りない回路が山ほどあることに気が付いた。っていうか命令5個しかないし、そりゃそーだ。CPUなめんな。
足りないものを少しずつ追加していくにはやっぱり独自アーキテクチャのほうが都合がよいということで、しばらくはオレオレアーキテクチャで進めることにした。
今回のネタはシフタ。
論理シフト
例えば1bit固定でシフトする回路は、入力と出力の線を1本ずらして接続すればよい。簡単である。2bitシフトする場合は2本ずらす。8bitレジスタで指定ビットシフトする命令を作る場合、左右合わせてこの回路を16個ほど作れば解決である。
以下、生成時にシフトビット数を指定するクラス。論理シフトするとあまったところはLで埋められる。
# 指定ビット論理シフトする(-が左、+が右) class LogicalShifter def initialize(bit_width, n) @buf = Buffer.new(bit_width) @n = n end def d=(v) @buf.d = Array.new(@buf.o.size) do |i| if i - @n < 0 or i - @n >= @buf.o.size OutputPin.new(L) else v[i - @n] end end end def o @buf.o end def inspect o.map{|o|"#{HLHASH[o.get]}"}.join end end _vcc = Line.new _gnd = Line.new vcc = _vcc.o gnd = _gnd.o rshifter = LogicalShifter.new(8, 2) rshifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] lshifter = LogicalShifter.new(8, -2) lshifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] _vcc.d = H _gnd.d = L p rshifter # LLHLLHLL p lshifter # LHLLLLLL
算術シフト
算術シフトというのは符号を保存したシフトで、例えば-8(2の補数で0xf8)を右シフトすると-4(2の補数で0xfc)になる。符合を保存するというのは、右シフトの場合にあまったビットに最上位のビットがコピーされることを意味する。プラスの場合や左シフトの場合は0で埋められるので、マイナスの値の右シフトの場合でのみ意味がある。
ところで、算術左シフトについて調べてみると、「符号ビットはそのまま」、と書いてあるサイトが結構多い。
世の中のCPUでは右シフトは算術/論理の命令があっても、左シフトは論理シフトしかない、というものが多い。実際のところ、左シフトに関しては符号を残す必要がないので算術でも論理でも同じロジックになる。なぜかというと、マイナスの値の場合、上位から2ビット目が0だったとしたら、2倍したらオーバーフローするからである。符号を残しても結果の値に意味が無い。
などといいつつ算術シフト回路は左右に対応しておく。
# 指定ビット算術シフトする(-が左、+が右) class ArithmeticShifter def initialize(bit_width, n) @buf = Buffer.new(bit_width) @n = n end def d=(v) @buf.d = Array.new(@buf.o.size) do |i| if i - @n < 0 or i - @n >= @buf.o.size if @n > 0 # 右シフトの場合 v[0] else OutputPin.new(L) end else v[i - @n] end end end def o @buf.o end def inspect o.map{|o|"#{HLHASH[o.get]}"}.join end end _vcc = Line.new _gnd = Line.new vcc = _vcc.o gnd = _gnd.o rshifter = ArithmeticShifter.new(8, 2) rshifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] lshifter = ArithmeticShifter.new(8, -2) lshifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] _vcc.d = H _gnd.d = L p rshifter # HHHLLHLL p lshifter # LHLLLLLL
論理算術共有シフト
16個作るとしても、論理シフトと算術シフトを分けて作ると倍になる。右シフトの場合に左端に埋める値を入力線で渡してもらうようにすると、実行時に論理と算術を指定することができるようになる。論理シフトの場合にL固定、算術シフトの場合は最上位ビットを入力することになる。命令によって接続する線をマルチプレクサで選択してやれば、論理と算術シフト回路は共有できる。これで生成する回路は半分になる。
# 指定ビットシフトする(論理、算術共有)(-が左、+が右) class Shifter def initialize(bit_width, n) @buf = Buffer.new(bit_width) @n = n @sign = Buffer1.new end def d=(v) @buf.d = Array.new(@buf.o.size) do |i| if i - @n < 0 @sign.o elsif i - @n >= @buf.o.size OutputPin.new(L) else v[i - @n] end end end # 右シフトの場合、ここに最上位ビットの出力ピンを接続すると算術シフト、L固定にすると論理シフト。 # 左シフトの場合は使われないのでどっちでもいい。 def sign=(v) @sign.d = v end def o @buf.o end def inspect o.map{|o|"#{HLHASH[o.get]}"}.join end end _vcc = Line.new _gnd = Line.new vcc = _vcc.o gnd = _gnd.o rshifter1 = Shifter.new(8, 2) rshifter1.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] rshifter1.sign = vcc rshifter2 = Shifter.new(8, 2) rshifter2.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] rshifter2.sign = gnd lshifter = Shifter.new(8, -2) lshifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] lshifter.sign = vcc _vcc.d = H _gnd.d = L p rshifter1 # HHHLLHLL p rshifter2 # LLHLLHLL p lshifter # LHLLLLLL
テストでは最上位ビットを入力するのが面倒だったので直接指定しちゃってるけども。
バレルシフタ
それでも16個生成するといかにも多い。無駄っぽい。そこで、バレルシフタという回路を構成する。賢い人がいたもんだ。検索するといろいろ出てくるが、ここの資料(pdf)がわかりやすかった。
シフト量も回路に入力して、シフト量のbitの数だけシフタ(シフト量は2の累乗)を構成し、1になってるビットのシフタを通す、というものである。
# バレルシフタ(右シフト限定) class BarrelShifterRight def initialize(shift_bit_width, data_bit_width) @shifter = Array.new(shift_bit_width){|i| Shifter.new(data_bit_width, 2**i)} @mux = Array.new(shift_bit_width){Mux1_n.new(data_bit_width)} @buf = Buffer.new(shift_bit_width) # 接続 shift_bit_width.times do |i| @mux[i].s = @buf.o[i] if i > 0 @shifter[i].d = @mux[i-1].o @mux[i].d = [@shifter[i].o, @mux[i-1].o] end end end def d=(v) @shifter[0].d = v @mux[0].d = [@shifter[0].o, v] end def shift=(v) @buf.d = v.reverse end def sign=(v) @shifter.each do |s| s.sign = v end end def o @mux.last.o end def inspect o.map{|o|"#{HLHASH[o.get]}"}.join end end _vcc = Line.new _gnd = Line.new vcc = _vcc.o gnd = _gnd.o s0 = Line.new s1 = Line.new s2 = Line.new shift = [s2.o, s1.o, s0.o] rshifter = BarrelShifterRight.new(shift.size, 8) rshifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] rshifter.sign = vcc rshifter.shift = shift _vcc.d = H _gnd.d = L # 4bit右シフト s0.d = L s1.d = L s2.d = H p rshifter # HHHHHLLH
これならシフタの数はシフト量のビットの数ぶんだけになるので、8bitレジスタの場合は3個で済む。左右あわせても6個だ。すばらしい。さっきのコードは右シフトのみだが、これを2つ突っ込んで、方向ビットも入力して、マルチプレクサで選択してやるようにするとRuby上のオブジェクトは1個になる。
# バレルシフタ class BarrelShifter def initialize(shift_bit_width, data_bit_width) # 左シフタ @lshifter = Array.new(shift_bit_width){|i| Shifter.new(data_bit_width, -(2**i))} @lmux = Array.new(shift_bit_width){Mux1_n.new(data_bit_width)} @lbuf = Buffer.new(shift_bit_width) # 右シフタ @rshifter = Array.new(shift_bit_width){|i| Shifter.new(data_bit_width, 2**i)} @rmux = Array.new(shift_bit_width){Mux1_n.new(data_bit_width)} @rbuf = Buffer.new(shift_bit_width) # 接続 shift_bit_width.times do |i| @lmux[i].s = @lbuf.o[i] @rmux[i].s = @rbuf.o[i] if i > 0 @lshifter[i].d = @lmux[i-1].o @lmux[i].d = [@lshifter[i].o, @lmux[i-1].o] @rshifter[i].d = @rmux[i-1].o @rmux[i].d = [@rshifter[i].o, @rmux[i-1].o] end end # 左右選択マルチプレクサ @mux = Mux1_n.new(data_bit_width) @mux.d = [@rmux.last.o, @lmux.last.o] end # データ入力 def d=(v) @lshifter[0].d = @rshifter[0].d = v @lmux[0].d = @rmux[0].d = [@rshifter[0].o, v] end # シフトビット数 def shift=(v) @lbuf.d = @rbuf.d = v.reverse end # 算術シフトの場合に最上位に埋められる符号ビット def sign=(v) @lshifter.each do |s| s.sign = v end @rshifter.each do |s| s.sign = v end end # Lが左、Hが右 def direction=(v) @mux.s = v end def o @mux.o end def inspect o.map{|o|"#{HLHASH[o.get]}"}.join end end _vcc = Line.new _gnd = Line.new vcc = _vcc.o gnd = _gnd.o s0 = Line.new s1 = Line.new s2 = Line.new shift = [s2.o, s1.o, s0.o] d0 = Line.new direction = d0.o shifter = BarrelShifter.new(shift.size, 8) shifter.d = [vcc, gnd, gnd, vcc, gnd, gnd, gnd, gnd] shifter.sign = vcc shifter.shift = shift shifter.direction = direction _vcc.d = H _gnd.d = L # 4bit右シフト s0.d = L s1.d = L s2.d = H d0.d = H p shifter # HHHHHLLH # 2bit左シフト s0.d = L s1.d = H s2.d = L d0.d = L p shifter # LHLLLLLL
できた。
これで論理シフトと算術シフトの命令を追加してやればシフト命令は完成である。
そういえばバレルシフタを調べるとローテイトさせる回路も出てくるのだが、シフトとローテイトを共有するやり方はよくわからない。よくわからないのでローテイト命令を作る場合はバレルローテイタでも作るか。