class RPicSim::Sim

This class represents a PIC microcontroller simulation. This class keeps track of the state of the simulation and provides methods for running the simulation, reading the state, and changing the state.

@api public

Attributes

eeprom[R]

Returns a {Memory} object that allows direct reading and writing of the bytes in the simulated EEPROM. @return [Memory]

pc[R]

Gets the program counter, an object that lets you read and write the current address in program space that is being executed. @return [RPicSim::ProgramCounter]

program_memory[R]

Returns a {Memory} object that allows direct reading and writing of the data in the program memory. Besides the main program, the program memory also contains the configuration words and the user IDs. @return [Memory]

ram[R]

Returns a {Memory} object that allows direct reading and writing of the bytes in the simulated RAM. @return [Memory]

stack_memory[R]

Returns a {Memory} object that allows direct reading and writing of the bytes in the simulated hardware call stack. @return [Memory]

stack_pointer[R]

Returns a {StackPointer} object that is like {#stkptr} but it works consistently across all PIC devices. The initial value is always 0 when the stack is empty and it points to the first unused space in the stack. @return [StackPointer]

stkptr[R]

Returns a {Variable} object corresponding to the stack pointer register. You can use this to read and write the value of the stack pointer. @return [Variable]

wreg[R]

Returns a {Variable} object corresponding to WREG. You can use this to read and write the value of the W register. @return [Variable]

Public Class Methods

new() click to toggle source

Makes a new simulation using the settings specified when the class was defined.

# File lib/rpicsim/sim.rb, line 270
def initialize
  @assembly = Mplab::MplabAssembly.new(device)
  @assembly.start_simulator_and_debugger(filename)
  @simulator = @assembly.simulator
  @processor = @simulator.processor

  initialize_memories
  initialize_pins
  initialize_sfrs_and_nmmrs
  initialize_vars

  @pc = ProgramCounter.new @simulator.processor

  @step_callbacks = []

  @stack_pointer = StackPointer.new(stkptr)
end

Public Instance Methods

convert_condition_to_proc(c) click to toggle source

Converts the specified condition into a Proc that, when called, will return a truthy value if the condition is satisfied. This is a helper for processing the main argument to {#run_to}. @param c One of the following:

- The symbol +:return+.
  The condition will be true if the current subroutine has returned.
  This is implemented by looking to see whether the stack pointer has
  decreased one level below the level it was at when this method was called.
- The name of a program label, as a symbol or string, or a
  {Label} object.  The condition will be true if the {#pc}
  value is equal to the label address.
- An integer representing an address.  The condition will be true if the
  {#pc} value is equal to the address.
- A Proc.  The Proc will be returned unchanged.

@return [Integer]

# File lib/rpicsim/sim.rb, line 646
def convert_condition_to_proc(c)
  case c

  when Proc
    c

  when Integer
    proc { pc.value == c }

  when :return
    current_val = stack_pointer.value
    if current_val == 0
      raise 'The stack pointer is 0; waiting for a return would be strange and might not work.'
    else
      target_val = current_val - 1
    end
    proc { stack_pointer.value == target_val }

  when Label
    convert_condition_to_proc c.address

  when String, Symbol
    convert_condition_to_proc label(c).address

  else
    raise ArgumentError, "Invalid run-termination condition #{c.inspect}"
  end
end
cycle_count() click to toggle source

Returns the number of instruction cycles simulated in this simulation. @return [Integer]

# File lib/rpicsim/sim.rb, line 388
def cycle_count
  @simulator.stopwatch_value
end
device() click to toggle source

Returns a string like “PIC10F322” specifying the PIC device number. @return [String]

# File lib/rpicsim/sim.rb, line 259
def device
  self.class.device
end
every_step(&proc) click to toggle source

Registers a new callback to be run after every simulation step. Each time the simulation takes a step, the provided block will be called.

# File lib/rpicsim/sim.rb, line 394
def every_step(&proc)
  @step_callbacks << proc
end
filename() click to toggle source

Returns the path to the firmware file. @return [String]

# File lib/rpicsim/sim.rb, line 265
def filename
  self.class.filename
end
goto(location) click to toggle source

Changes the {#pc} value to be equal to the address of the given location. @param location Any valid argument to {#location_address}.

# File lib/rpicsim/sim.rb, line 508
def goto(location)
  pc.value = location_address(location)
end
inspect() click to toggle source

@return [String]

# File lib/rpicsim/sim.rb, line 627
def inspect
  "#<#{self.class}:0x%x, #{pc_description}, stack_pointer = #{stack_pointer.value}>" % object_id
end
label(name) click to toggle source

Returns a {Label} object if a program label by that name is found. The name is specified in the code that defined the label. If you are using a C compiler, you will probably need to prefix the name with an underscore. @return [Label]

# File lib/rpicsim/sim.rb, line 377
def label(name)
  program_file.label(name)
end
labels() click to toggle source

Returns a hash that associates label names as Ruby symbols to {Label} objects.

# File lib/rpicsim/sim.rb, line 382
def labels
  program_file.labels
end
location_address(location) click to toggle source

Gets the address of the specified location in program memory. This is a helper for processing the main argument to {#goto} and {#run_subroutine}. @param location One of the following:

- The name of a program label, as a symbol or string.
- A {Label} object.
- An integer representing the address.

@return [Integer]

# File lib/rpicsim/sim.rb, line 498
def location_address(location)
  case location
  when Integer         then location
  when Label           then location.address
  when Symbol, String  then label(location).address
  end
end
new_ram_watcher() click to toggle source

Creates and returns a {MemoryWatcher} object configured to watch for changes to RAM. For more information, see {file:RamWatcher.md}. @return [MemoryWatcher]

# File lib/rpicsim/sim.rb, line 678
def new_ram_watcher
  MemoryWatcher.new(self, @simulator.fr_memory, ram_vars + sfr_vars)
end
pc_description() click to toggle source

Generates a friendly human-readable string description of where the program counter is currently using the symbol table.

# File lib/rpicsim/sim.rb, line 565
def pc_description
  program_file.address_description(pc.value)
end
pin(name) click to toggle source

Returns a Pin object if a pin by that name is found, or raises an exception. @param name [Symbol] The name from the datasheet or a name specified in a

call to {ClassDefinitionMethods#def_pin} in the class definition.

@return [Pin]

# File lib/rpicsim/sim.rb, line 352
def pin(name)
  @pins_by_name[name.to_sym] or raise ArgumentError, "Cannot find pin named '#{name}'."
end
program_file() click to toggle source

Returns the {RPicSim::ProgramFile} representing the firmware being simulated. @return [ProgramFile]

# File lib/rpicsim/sim.rb, line 688
def program_file
  self.class.program_file
end
reg(name) click to toggle source

Returns a {Variable} object if a Special Function Register (SFR) or Non-Memory-Mapped Register (NMMR) by that name is found. If the register cannot be found, this method raises an exception. @param name [Symbol] The name from the datasheet. @return [Register]

# File lib/rpicsim/sim.rb, line 361
def reg(name)
  name = name.to_sym
  @sfrs[name] || @nmmrs[name] or raise ArgumentError, "Cannot find SFR or NMMR named '#{name}'."
end
return() click to toggle source

Simulates a return instruction being executed by popping the top value off of the stack and setting the {#pc} value equal to it. This can be useful for speeding up your tests when you have a very slow function and just want to skip it.

# File lib/rpicsim/sim.rb, line 552
def return
  if stack_pointer.value == 0
    raise 'Cannot return because stack is empty.'
  end

  # Simulate popping the stack.
  stack_pointer.value -= 1
  pc.value = @stack_memory.read_word(stack_pointer.value)
  update_top_of_stack_registers
end
run_cycles(num_cycles) click to toggle source

Runs the simulation for the given number of instruction cycles. Note that the existence of multi-cycle instructions means that sometimes this method can run one cycle longer than desired. @param num_cycles [Integer]

# File lib/rpicsim/sim.rb, line 537
def run_cycles(num_cycles)
  run_to_cycle_count cycle_count + num_cycles
end
run_steps(step_count) click to toggle source

Executes the specified number of instructions. @param step_count [Integer] @return nil

# File lib/rpicsim/sim.rb, line 409
def run_steps(step_count)
  step_count.times { step }
  nil  # To make using the ruby debugger more pleasant.
end
run_subroutine(location, opts = {}) click to toggle source

Runs the subroutine at the given location. This can be useful for doing unit tests of subroutines in your firmware.

The current program counter value will be pushed onto the stack before running the subroutine so that after the subroutine is done the simulation can proceed as it was before.

Example usage in RSpec:

run_subroutine :calculateSum, cycle_limit: 20
sum.value.should == 30

@param location Any valid argument to {#location_address}. It should

generally point to a subroutine in program memory that will end by
executing a return instructions.

@param opts Any of the options supported by {#run_to}.

# File lib/rpicsim/sim.rb, line 527
def run_subroutine(location, opts = {})
  stack_push pc.value
  goto location
  run_to :return, opts
end
run_to(conditions, opts = {}) click to toggle source

Runs the simulation until one of the given conditions has been met, then stops and returns the condition that was met.

Example usage in RSpec:

result = run_to [:mylabel, :return], cycle_limit: 400
result.should == :return

@param conditions Each element of the conditions array should be

a Proc that returns true when the condition is met, a symbol corresponding
to a program label, or any other object that is a valid argument to
{#convert_condition_to_proc}.
If there is only one condition, you can pass it directly in as the first
argument without wrapping it in an array.

@param opts [Hash] A hash of options.

- +cycle_limit+: The maximum number of cycles to run, as an integer.
  It is recommended to always specify this to avoid accidentally
  making an infinite loop.  Note that multi-cycle instructions mean
  that this limit will sometimes be violated by one cycle.
  If none of the conditions are met by the cycle limit, an exception is raised.
- +cycles+: A range of integers specifying how long you expect
  it to take to reach one of the conditions, for example e.g. +1000..2000+.
  If a condition is met before the minimum, an exception is raised.
  If none of the conditions are met after the maximum, an exception is
  raised.

  This option is a more powerful version of +cycle_limit+, so it cannot
  be used at the same time as +cycle_limit+.

@return The condition that was met which caused the run to stop.

# File lib/rpicsim/sim.rb, line 443
def run_to(conditions, opts = {})
  conditions = Array(conditions)
  if conditions.empty?
    raise ArgumentError, 'Must specify at least one condition.'
  end

  condition_procs = conditions.map(&method(:convert_condition_to_proc))

  allowed_keys = [:cycle_limit, :cycles]
  invalid_keys = opts.keys - allowed_keys
  if !invalid_keys.empty?
    raise ArgumentError, "Unrecognized options: #{invalid_keys.join(", ")}"
  end

  if opts[:cycles] && opts[:cycle_limit]
    raise ArgumentError, 'Cannot specify both :cycles and :cycle_limit.'
  end

  start_cycle = cycle_count
  if opts[:cycles]
    raise "Invalid range: #{opts[:cycles].inspect}." unless opts[:cycles].min && opts[:cycles].max
    min_cycle = start_cycle + opts[:cycles].min
    max_cycle = start_cycle + opts[:cycles].max
    max_cycle -= 1 if opts[:cycles].exclude_end?
  elsif opts[:cycle_limit]
    max_cycle = start_cycle + opts[:cycle_limit] if opts[:cycle_limit]
  end

  # Loop until one of the conditions is satisfied.
  until (met_condition_index = condition_procs.find_index(&:call))
    if max_cycle && cycle_count >= max_cycle
      raise "Failed to reach #{conditions.inspect} after #{cycle_count - start_cycle} cycles."
    end

    step
  end

  met_condition = conditions[met_condition_index]

  if min_cycle && cycle_count < min_cycle
    raise "Reached #{met_condition.inspect} in only #{cycle_count - start_cycle} cycles " +
      "but expected it to take at least #{min_cycle - start_cycle}."
  end

  # Return the argument that specified the condition that was satisfied.
  met_condition
end
run_to_cycle_count(count) click to toggle source

Runs the simulation until the {#cycle_count} is greater than or equal to the given cycle count. @param count [Integer]

# File lib/rpicsim/sim.rb, line 544
def run_to_cycle_count(count)
  step while cycle_count < count
end
shortcuts() click to toggle source
# File lib/rpicsim/sim.rb, line 682
def shortcuts
  self.class::Shortcuts
end
stack_contents() click to toggle source

Gets the contents of the stack as an array of integers. @return [Array(Integer)] An array of integers.

# File lib/rpicsim/sim.rb, line 582
def stack_contents
  (0...stack_pointer.value).map do |n|
    @stack_memory.read_word(n)
  end
end
stack_push(value) click to toggle source

Pushes the given address onto the simulated call stack.

# File lib/rpicsim/sim.rb, line 570
def stack_push(value)
  if !@stack_memory.valid_address?(stack_pointer.value)
    raise "Simulated stack is full (stack pointer = #{stack_pointer.value})."
  end

  @stack_memory.write_word(stack_pointer.value, value)
  stack_pointer.value += 1
  update_top_of_stack_registers
end
stack_trace() click to toggle source

Returns a call stack trace representing the current state of the simulation. Printing this stack trace can help you figure out what part of your code is running and why. @return [StackTrace]

# File lib/rpicsim/sim.rb, line 592
def stack_trace
  # The stack stores return addresses, not call addresses.
  # We get the call addresses by subtracting the address increment,
  # which is the number of address units that each word of program memory takes up.
  addresses = stack_contents.map do |return_address|
    return_address - address_increment
  end
  addresses << pc.value
  entries = addresses.map do |address|
    StackTraceEntry.new address, program_file.address_description(address)
  end
  StackTrace.new(entries)
end
step() click to toggle source

Executes one more instruction. @return nil

# File lib/rpicsim/sim.rb, line 400
def step
  @assembly.debugger_step
  @step_callbacks.each(&:call)
  nil  # To make using the ruby debugger more pleasant.
end
var(name) click to toggle source

Returns a {Variable} object if a variable by that name is found. If the variable cannot be found, this method raises an exception. @return [Variable]

# File lib/rpicsim/sim.rb, line 369
def var(name)
  @vars[name.to_sym] or raise ArgumentError, "Cannot find var named '#{name}'."
end

Private Instance Methods

address_increment() click to toggle source
# File lib/rpicsim/sim.rb, line 703
def address_increment
  @assembly.device_info.code_address_increment
end
initialize_memories() click to toggle source
# File lib/rpicsim/sim.rb, line 290
def initialize_memories
  # Set up our stores and helper objects.
  @ram = Memory.new @simulator.fr_memory
  @eeprom = Memory.new @simulator.eeprom_memory
  @sfr_memory = Memory.new @simulator.sfr_memory
  @nmmr_memory = Memory.new @simulator.nmmr_memory
  @stack_memory = Memory.new @simulator.stack_memory

  # config_memory must be before test_memory, because test_memory provides
  # bad values for the configuration words.
  @program_memory = Memory.new CompositeMemory.new [
    @simulator.program_memory,
    @simulator.config_memory,
    @simulator.test_memory,
  ]
end
initialize_pins() click to toggle source
# File lib/rpicsim/sim.rb, line 307
def initialize_pins
  pins = @simulator.pins.map { |mplab_pin| Pin.new(mplab_pin) }

  @pins_by_name = {}
  pins.each do |pin|
    pin.names.each do |name|
      @pins_by_name[name.to_sym] = pin
    end
  end

  self.class.pin_aliases.each do |our_name, datasheet_name|
    @pins_by_name[our_name] = @pins_by_name[datasheet_name] or raise "Pin #{datasheet_name} not found."
  end
end
initialize_sfrs_and_nmmrs() click to toggle source
# File lib/rpicsim/sim.rb, line 331
def initialize_sfrs_and_nmmrs
  @sfrs = {}
  @assembly.device_info.sfrs.each do |sfr|
    @sfrs[sfr.name.to_sym] = Variable.new Storage::Register.new @processor.get_sfr(sfr.name), @sfr_memory, sfr.width
  end

  @nmmrs = {}
  @assembly.device_info.nmmrs.each do |nmmr|
    @nmmrs[nmmr.name.to_sym] = Variable.new Storage::Register.new @processor.get_nmmr(nmmr.name), @nmmr_memory, nmmr.width
  end

  @wreg = reg(:WREG)
  @stkptr = reg(:STKPTR)
end
initialize_vars() click to toggle source
# File lib/rpicsim/sim.rb, line 322
def initialize_vars
  memories = {
    ram: ram,
    program_memory: program_memory,
    eeprom: eeprom,
  }
  @vars = self.class.variable_set.bind(memories)
end
ram_vars() click to toggle source
# File lib/rpicsim/sim.rb, line 694
def ram_vars
  ram_var_names = self.class.variable_set.var_names_for_memory(:ram)
  @vars.values_at(*ram_var_names)
end
sfr_vars() click to toggle source
# File lib/rpicsim/sim.rb, line 699
def sfr_vars
  @sfrs.values
end
update_top_of_stack_registers() click to toggle source

Update the TOSU:TOSH:TOSL registers because the simulator uses those (if they exist) when simulating a return instruction.

# File lib/rpicsim/sim.rb, line 610
def update_top_of_stack_registers
  return unless @sfrs.key?(:TOSL)

  tos = if stack_pointer.value == 0
          0
        else
          @stack_memory.read_word(stack_pointer.value - 1)
        end

  reg(:TOSL).value = tos >> 0 & 0xFF
  reg(:TOSH).value = tos >> 8 & 0xFF
  reg(:TOSU).value = tos >> 16 & 0xFF if @sfrs.key?(:TOSU)
end