MC logo

Gate Classes

^  Ruby Example Code

<<Setting Variables csim.rb Circuit Test I>>
#
# Ruby circuit simulation classes.  This file contains a base class Gate,
# and several derived classes describing digital logic gates.  There are
# also classes for input and display.  There's also a flip-flop.
#

class Gate 
  # This is a count of the "active" gates, which are ones which have received
  # a signal but not resolved it.
  @@active = 0

  # This is a list of gates which have registered that they want to be
  # notified when the circuit is quiet.  They give an integer priority,
  # and are notified in increasing priority order.
  @@needquiet = { }
  def quiet_register(pri)
    if ! @@needquiet.key?(pri) then @@needquiet[pri] = [ ] end
    @@needquiet[pri].push(self)
  end

  # Here's how we set stuff.  There are static and object versions of
  # each, since I may want to activate from other spots.
  def Gate.activate
    @@active += 1
  end
  def activate
    Gate.activate
  end
  def Gate.deactivate
    @@active -= 1
    if @@active == 0 then
      @@needquiet.keys.sort.each \
                { |p| @@needquiet[p].each { |g| g.onquiet } }
    end
  end
  def deactivate
    Gate.deactivate
  end

  # This is the default quiet action (nothing).
  def onquiet
  end

  # A signal is directed to a particular port on a particular gate.  This
  # encapsulates those two data.  When a gate connects to us, we send back
  # one of these to direct its later signal changes.
  class LinkHandle
    def initialize(sink_gate, sink_port)
      @sinkg = sink_gate
      @sinkp = sink_port
    end

    # The sending gate uses this method to forward the signal to the
    # downstream gate.
    def signal(value)
      @sinkg.signal(@sinkp,value)
    end

    attr_reader :sinkg, :sinkp
  end

  def initialize(ival = false)
    @inputs = [ ]       # Array of inputs (boolean values)
    @outputs = [ ]      # Array of LinkHandle objects where to send output
    @outval = ival      # Present output value.
  end

  # This is called when a input gate sends us a signal on a particular input.
  # We recompute our output value, and, if it changes, we send it on to all
  # of our outbound connections.
  def signal(port, val)
    # The derived class needs to implement the value method.
    self.activate
    @inputs[port] = val
    newval = self.value
    if newval != @outval then
      @outval = newval
      @outputs.each { | c | c.signal(newval) }
    end
    self.deactivate
  end

  # Call this to connect your output to the next one of our inputs.
  def connect(v)
    port = @inputs.length
    @inputs.push(v)
    c = LinkHandle.new(self, port)
    self.signal(port, v)
    return c
  end

  # Join me to another gate.
  def join(g)
    @outputs.push(g.connect(@outval))
  end
  def joinmany(*p)
    p.each { |i| self.join(i); }
  end

  attr_reader :outval

  # Some printing help
  def name
    return self.class.to_s
  end
  def insstr
    return (if @inputs.length == 0 then "-" else @inputs.join('.') end)
  end
  def to_s
    return name + " " + insstr + " => " + @outval.to_s
  end

  # Create another object of the same type.  
  def another
    return self.class.new
  end
  def manyothers(n)
    ret = []
    n.times { ret.push(self.another) }
    return ret
  end

  # This manufactures any number of objects.  It is a static method, and
  # inherited by the real gates.  The expression self.new, then, runs the
  # new method on the actual object, which the inheriting class.  Therefore,
  # it will create any gate.
  def Gate.many(n)
    ret = [ ]
    n.times { ret.push(self.new) }
    return ret
  end
  
  # Dump a whole circuit.  Yecch.
  def outlinks
    return @outputs
  end
  def Gate.dump(*roots)
    ct = -1
    gatemap = { }
    for g in roots
      gatemap[g] = (ct += 1) unless gatemap.has_key?(g)
    end

    printed = { }
    while roots.length > 0 
      g = roots.shift 
      next if printed.has_key?(g)
      print "[", gatemap[g], "] ", g, ":"
      for c in g.outlinks
        og = c.sinkg
        gatemap[og] = (ct += 1) unless gatemap.has_key?(og)
        print " ", gatemap[og], "@", c.sinkp
        roots.push(og)
      end
      print " [none]" if g.outlinks.length <= 0
      print "\n"
      printed[g] = true
    end
  end
end

# Standard and gate
class AndGate < Gate
  def initialize
    super(true)
  end
  def value
    for i in @inputs
      return false if !i
    end
    return true
  end
end
class NandGate < AndGate
  def value
    return ! super
  end
end

# Standard or gate
class OrGate < Gate
  def value
    for i in @inputs
      return true if i
    end
    return false
  end
end
class NorGate < OrGate
  def value
    return ! super
  end
end

# Standard xor gate
class XorGate < Gate
  def value
    ret = false
    for i in @inputs
      ret ^= i
    end
    return ret
  end
end

# Gates with a limited number of input connections.
class LimitedGate < Gate
  def initialize(max=1,i=false)
    super(i)
    @max = max
  end

  # Enforce connect limit.
  def connect(v)
    if @inputs.length >= @max then
      raise TypeError.new("Too many input connections.")
    end
    super(v)
  end
  
end

# Not gate.
class NotGate < LimitedGate
  def initialize
    super(1,true)
  end
  def value
    return ! @inputs[0]
  end
end

# This is a "yes gate" or amplifier.  It just forwards its input to all its
# outputs
class Connector < LimitedGate
  def value
    return @inputs[0]
  end

  # We can also use it as a one-bit input device.
  def send(v)
    self.signal(0,v)
  end
end

# D Flip-Flop.  Level-triggered.  First input is D, second is clock.
class FlipFlop < LimitedGate
  def initialize
    super(2)
  end
  def value
    return (if @inputs[1] then @inputs[0] else @outval end)
  end
end

# D Flip-Flop.  Edge-triggered.  First input is D, second is clock.
# I think the level-triggered might make a lot more sense with this
# simulation, though these are better in circuits.  
class FlipFlopET < FlipFlop
  def initialize
    super
    @newval = false
  end
  def value
    return @newval
  end
  def signal(port, val)
    # Need to stick our fingers in this thing to find the rising edge.
    self.activate
    @newval = 
      if port == 1 && !@inputs[1] && val then @inputs[0] else @outval  end
    super(port,val)
    self.deactivate
  end
end

# Simple test point
class Tester < LimitedGate
  def initialize(name="Tester")
    super(1)
    @name = name
  end
  attr_writer :name
  def value
    print @name, ": ", if @inputs[0] then "on" else "off" end, "\n";
    return @inputs[0]
  end
end

# Numeric output device.  Connect lines starting with LSB.
class NumberOut < Gate
  @@quiet = false
  def NumberOut.shush(q=true)
    @@quiet = q
  end

  def initialize(name="Value", pri = 1)
    @name = name
    quiet_register(pri)
    super()
  end

  attr_writer :name

  # Print the value on quiet.
  def onquiet
    return if @@quiet;

    val = 0
    @inputs.reverse_each { |i| 
      val <<= 1
      if i then
        val |= 1
      end
    }

    print @name, ": ", val, "\n"
  end
  def value
    return false
  end
end

# LED which prints when circuit becomes quiet.
class LED < NumberOut
  def initialize(name="LED", pri = 1)
    super(name, pri)
  end
  def onquiet
    if @inputs.length > 0 && ! @@quiet then
      print @name, ": ", if @inputs[0] then "on" else "off" end, "\n"
    end
  end
  def connect(v)
    if @inputs.length >= 1 then
      raise TypeError.new("Too many input connections.")
    end
    super(v)
  end
end

# Base for input devices.  Mostly deals will collecting connections.
class InputDevice
  def initialize
    @targs = []
  end

  # Add a connection
  def join(g)
    @targs.push(g.connect(false))
  end
  def joinmany(*p)
    p.each { |i| self.join(i); }
  end

  def outlinks
    return @targs
  end

end

# Switch bank.  Connects to any number of gates, and will feed them a
# binary number (as a string).  Connections start with LSB.  Initially,
# all the switches are off.
class SwitchBank < InputDevice

  # Send a number.  Can take an integer or a string.
  def set(n)
    if n.is_a?(TrueClass) || n.is_a?(FalseClass) then
      @targs.each { | x | x.signal(n) }
    elsif n.is_a?(Integer) then
      @targs.each { | x | x.signal(n&1 == 1); n >>= 1 }
    else
      # Assume n is an ascii string of 1's and 0's.
      if n.length < @targs.length then
        n = ('0' * (@targs.length - n.length)) + n
      end
      sub = n.length - 1
      @targs.each { | x | x.signal(n[sub].chr != "0"); sub -= 1 }
    end
  end

  # This is like switch, but it keeps the circuit active during each
  # sending.
  def value=(n)
    Gate.activate
    self.set(n)
    Gate.deactivate
  end

end

# Send a pulse (clock tick?)
class Pulser < InputDevice
  
  def pulse
    Gate.activate
    @targs.each { |t| t.signal(true); }
    @targs.each { |t| t.signal(false); }
    Gate.deactivate
  end
end
<<Setting Variables Circuit Test I>>