rpicsim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +36 -0
- data/Gemfile +10 -0
- data/Introduction.md +64 -0
- data/LICENSE.txt +24 -0
- data/README.md +66 -0
- data/docs/ChangeLog.md +10 -0
- data/docs/Contributing.md +30 -0
- data/docs/Debugging.md +96 -0
- data/docs/DefiningSimulationClass.md +84 -0
- data/docs/DesignDecisions.md +48 -0
- data/docs/HowMPLABXIsFound.md +14 -0
- data/docs/IntegrationTesting.md +15 -0
- data/docs/IntroductionToRSpec.md +203 -0
- data/docs/IntroductionToRuby.md +90 -0
- data/docs/KnownIssues.md +204 -0
- data/docs/Labels.md +39 -0
- data/docs/MakingTestsRunFaster.md +29 -0
- data/docs/Manual.md +38 -0
- data/docs/PersistentExpectations.md +100 -0
- data/docs/Pins.md +143 -0
- data/docs/PreventingCallStackOverflow.md +94 -0
- data/docs/QuickStartGuide.md +85 -0
- data/docs/RSpecIntegration.md +119 -0
- data/docs/RamWatcher.md +46 -0
- data/docs/Running.md +129 -0
- data/docs/SFRs.md +71 -0
- data/docs/Stubbing.md +161 -0
- data/docs/SupportedCompilers.md +5 -0
- data/docs/SupportedDevices.md +14 -0
- data/docs/SupportedMPLABXVersions.md +12 -0
- data/docs/SupportedOperatingSystems.md +3 -0
- data/docs/UnitTesting.md +9 -0
- data/docs/Variables.md +140 -0
- data/lib/rpicsim.rb +15 -0
- data/lib/rpicsim/call_stack_info.rb +306 -0
- data/lib/rpicsim/flaws.rb +76 -0
- data/lib/rpicsim/instruction.rb +178 -0
- data/lib/rpicsim/label.rb +28 -0
- data/lib/rpicsim/memory.rb +29 -0
- data/lib/rpicsim/memory_watcher.rb +98 -0
- data/lib/rpicsim/mplab.rb +138 -0
- data/lib/rpicsim/mplab/mplab_assembly.rb +102 -0
- data/lib/rpicsim/mplab/mplab_device_info.rb +40 -0
- data/lib/rpicsim/mplab/mplab_disassembler.rb +23 -0
- data/lib/rpicsim/mplab/mplab_instruction.rb +32 -0
- data/lib/rpicsim/mplab/mplab_memory.rb +33 -0
- data/lib/rpicsim/mplab/mplab_nmmr_info.rb +21 -0
- data/lib/rpicsim/mplab/mplab_observer.rb +12 -0
- data/lib/rpicsim/mplab/mplab_pin.rb +61 -0
- data/lib/rpicsim/mplab/mplab_processor.rb +30 -0
- data/lib/rpicsim/mplab/mplab_program_file.rb +35 -0
- data/lib/rpicsim/mplab/mplab_register.rb +25 -0
- data/lib/rpicsim/mplab/mplab_sfr_info.rb +21 -0
- data/lib/rpicsim/mplab/mplab_simulator.rb +102 -0
- data/lib/rpicsim/pin.rb +61 -0
- data/lib/rpicsim/program_counter.rb +19 -0
- data/lib/rpicsim/program_file.rb +160 -0
- data/lib/rpicsim/register.rb +78 -0
- data/lib/rpicsim/rspec.rb +11 -0
- data/lib/rpicsim/rspec/be_predicate.rb +12 -0
- data/lib/rpicsim/rspec/helpers.rb +48 -0
- data/lib/rpicsim/rspec/persistent_expectations.rb +53 -0
- data/lib/rpicsim/rspec/sim_diagnostics.rb +51 -0
- data/lib/rpicsim/search.rb +20 -0
- data/lib/rpicsim/sim.rb +702 -0
- data/lib/rpicsim/stack_trace.rb +32 -0
- data/lib/rpicsim/variable.rb +236 -0
- data/lib/rpicsim/version.rb +3 -0
- metadata +114 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'persistent_expectations'
|
2
|
+
|
3
|
+
module RPicSim
|
4
|
+
module RSpec
|
5
|
+
# This module gets included into your RSpec examples if you use the
|
6
|
+
# following line in your spec_helper.rb:
|
7
|
+
#
|
8
|
+
# require 'rpicsim/rspec'
|
9
|
+
#
|
10
|
+
# It provides the {#start_sim} method and includes all the methods from
|
11
|
+
# {PersistentExpectations}.
|
12
|
+
# See {file:RSpecIntegration.md} for more information about RPicSim's
|
13
|
+
# integration with RSpec.
|
14
|
+
module Helpers
|
15
|
+
include RPicSim::RSpec::PersistentExpectations
|
16
|
+
|
17
|
+
# This attribute allows you to type +sim+ in your specs instead of +@sim+ to
|
18
|
+
# get access to the {RPicSim::Sim} instance which represents the simulation.
|
19
|
+
# You must call {#start_sim} before using +sim+.
|
20
|
+
attr_reader :sim
|
21
|
+
|
22
|
+
# Starts a new simulation with the specified class, makes it
|
23
|
+
# accessible via the attribute {#sim}, and adds convenience
|
24
|
+
# methods using {#add_shortcuts}.
|
25
|
+
# @param klass [Class] This should be a subclass of {RPicSim::Sim} or at least act like it.
|
26
|
+
# @param args A list of arguments to pass on to the the +new+ method of the class.
|
27
|
+
# This should usually be empty unless you have modified your class to take arguments in its
|
28
|
+
# constructor.
|
29
|
+
def start_sim(klass, *args)
|
30
|
+
@sim = klass.new(*args)
|
31
|
+
add_shortcuts
|
32
|
+
sim.every_step { check_expectations }
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_shortcuts
|
36
|
+
configuration_value = ::RSpec.configuration.sim_shortcuts
|
37
|
+
case configuration_value
|
38
|
+
when :all then extend ::RPicSim::Sim::BasicShortcuts, sim.shortcuts
|
39
|
+
when :basic then extend ::RPicSim::Sim::BasicShortcuts
|
40
|
+
when :none
|
41
|
+
else
|
42
|
+
raise "Invalid sim_shortcuts configuration value: #{configuration_value.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module RPicSim
|
2
|
+
module RSpec
|
3
|
+
# This simple module is included in {RPicSim::RSpec::Helpers} so it is
|
4
|
+
# available in RSpec tests.
|
5
|
+
# It provides a way to set expectations on objects that will be checked after
|
6
|
+
# every step of the simulation, so you can make sure that the object stays in
|
7
|
+
# the state you are expecting it to.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# b = []
|
11
|
+
# expecting b => satisfy { |lb| lb.size < 10 }
|
12
|
+
# 20.times do
|
13
|
+
# b << 0
|
14
|
+
# check_expectations # => raises an error once b has 10 elements
|
15
|
+
# end
|
16
|
+
module PersistentExpectations
|
17
|
+
# Returns the current set of persistent expectations.
|
18
|
+
# The keys are the objects under test and the values are matchers that
|
19
|
+
# we expect to match the object.
|
20
|
+
# @return [Hash]
|
21
|
+
def expectations
|
22
|
+
@expectations ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Checks the expectations; if any object does not match its matcher then
|
26
|
+
# it raises an error.
|
27
|
+
def check_expectations
|
28
|
+
expectations.each do |subject, matcher|
|
29
|
+
if matcher
|
30
|
+
expect(subject).to matcher
|
31
|
+
end
|
32
|
+
end
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds or removes expectations.
|
37
|
+
# The argument should be a hash that associates objects to matchers.
|
38
|
+
# A matcher can be any bit of Ruby code that you would be able write in
|
39
|
+
# RSpec in place of MATCHER in the code below:
|
40
|
+
#
|
41
|
+
# expect(object).to MATCHER
|
42
|
+
#
|
43
|
+
# For example, you could do:
|
44
|
+
#
|
45
|
+
# expecting main_output_pin: be_driving_high
|
46
|
+
#
|
47
|
+
# To remove an expectation on an object, just provide +nil+ for the matcher.
|
48
|
+
def expecting(hash)
|
49
|
+
expectations.merge! hash
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# If an example fails, store some diagnostic information about the state of the
|
2
|
+
# simulation so we can print it later.
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.after(:each) do
|
5
|
+
if @sim && example.exception
|
6
|
+
if @sim.respond_to? :cycle_count
|
7
|
+
example.metadata[:sim_cycle_count] = @sim.cycle_count
|
8
|
+
end
|
9
|
+
if @sim.respond_to? :stack_trace
|
10
|
+
example.metadata[:sim_stack_trace] = @sim.stack_trace
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'rspec/core/formatters/base_text_formatter'
|
17
|
+
RSpec::Core::Formatters::BaseTextFormatter
|
18
|
+
|
19
|
+
# We enhance rspec's backtrace so it will also show us the stack trace of the
|
20
|
+
# simulation, if available. These monkey patches are broken up into three
|
21
|
+
# different functions so you can easily customize any of them without messing up
|
22
|
+
# the other ones.
|
23
|
+
class RSpec::Core::Formatters::BaseTextFormatter
|
24
|
+
alias dump_backtrace_without_sim_diagnostics dump_backtrace
|
25
|
+
|
26
|
+
def dump_backtrace(example)
|
27
|
+
dump_backtrace_without_sim_diagnostics(example)
|
28
|
+
dump_sim_diagnostics(example)
|
29
|
+
end
|
30
|
+
|
31
|
+
def dump_sim_diagnostics(example)
|
32
|
+
dump_sim_cycle_count(example)
|
33
|
+
dump_sim_stack_trace(example)
|
34
|
+
end
|
35
|
+
|
36
|
+
def dump_sim_cycle_count(example)
|
37
|
+
cycle_count = example.metadata[:sim_cycle_count] or return
|
38
|
+
output.puts long_padding
|
39
|
+
output.printf long_padding + "Simulation cycle count: %d\n", cycle_count
|
40
|
+
end
|
41
|
+
|
42
|
+
# Looks inside the metadata for the given RSpec example to see if a
|
43
|
+
# simulation stack trace was recorded. If so, it outputs it with the
|
44
|
+
# appropriate indentation.
|
45
|
+
def dump_sim_stack_trace(example)
|
46
|
+
sim_stack_trace = example.metadata[:sim_stack_trace] or return
|
47
|
+
output.puts long_padding
|
48
|
+
output.puts long_padding + "Simulation stack trace:"
|
49
|
+
sim_stack_trace.output(output, long_padding)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RPicSim
|
2
|
+
module Search
|
3
|
+
# Performs a depth first search.
|
4
|
+
# No measures are taken to avoid processing the same node multiple
|
5
|
+
# times, so this is only suitable for acyclic graphs.
|
6
|
+
# Every time a node is processed, it will be yielded as the first
|
7
|
+
# and only argument to the block.
|
8
|
+
#
|
9
|
+
# This is used by {CallStackInfo#code_paths} to search the instruction
|
10
|
+
# graph backwards in order to find code_paths for a given instruction.
|
11
|
+
def self.depth_first_search_simple(root_nodes)
|
12
|
+
unprocessed_nodes = root_nodes.reverse
|
13
|
+
while !unprocessed_nodes.empty?
|
14
|
+
node = unprocessed_nodes.pop
|
15
|
+
nodes = yield node
|
16
|
+
unprocessed_nodes.concat(nodes.reverse)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/rpicsim/sim.rb
ADDED
@@ -0,0 +1,702 @@
|
|
1
|
+
# TODO: for performance, consider making people explicitly enable (or construct?) the ram_watcher
|
2
|
+
# before they can use it. See if this would affect the run time of rcs03a specs before committing to it.
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
require_relative 'mplab'
|
7
|
+
require_relative 'flaws'
|
8
|
+
require_relative 'pin'
|
9
|
+
require_relative 'memory'
|
10
|
+
require_relative 'register'
|
11
|
+
require_relative 'variable'
|
12
|
+
require_relative 'program_counter'
|
13
|
+
require_relative 'label'
|
14
|
+
require_relative 'memory_watcher'
|
15
|
+
require_relative 'program_file'
|
16
|
+
require_relative 'stack_trace'
|
17
|
+
|
18
|
+
module RPicSim
|
19
|
+
# This class represents a PIC microcontroller simulation.
|
20
|
+
# This class keeps track of the state of the simulation and provides methods for
|
21
|
+
# running the simulation, reading the state, and changing the state.
|
22
|
+
# This the main class of RPicSim.
|
23
|
+
class Sim
|
24
|
+
|
25
|
+
# These methods should be called while defining a subclass of {Sim}.
|
26
|
+
module ClassDefinitionMethods
|
27
|
+
|
28
|
+
# Specifies what exact device the firmware runs on. In theory we could extract this
|
29
|
+
# from the COF file instead of requiring it to be specified in subclasses of {Sim}, but
|
30
|
+
# MPLAB X classes do not seem to make that easy.
|
31
|
+
# @param device [String] The device name, for example "PIC10F322".
|
32
|
+
def device_is(device)
|
33
|
+
@device = device
|
34
|
+
@assembly = Mplab::MplabAssembly.new(device)
|
35
|
+
@code_word_max_value = @assembly.device_info.code_word_max_value
|
36
|
+
end
|
37
|
+
|
38
|
+
# Specifies the path to the firmware file. The file can be a HEX or COF file, but
|
39
|
+
# COF is recommended so that you can access label addresses and other debugging information.
|
40
|
+
# You must call {#device_is} before calling this.
|
41
|
+
def filename_is(filename)
|
42
|
+
raise "Must specify device before filename (e.g. 'device_is \"PIC10F322\"')" unless @device
|
43
|
+
@filename = filename
|
44
|
+
initialize_symbols
|
45
|
+
end
|
46
|
+
|
47
|
+
# Define a pin alias.
|
48
|
+
#
|
49
|
+
# @param our_name [Symbol] Specifies what you would like to
|
50
|
+
# call the pin.
|
51
|
+
# A method with this name will be added to your class's +Shortcuts+ module so it
|
52
|
+
# is available as a method on instances of your class and also in your RSpec tests.
|
53
|
+
# @param datasheet_name [Symbol] A symbol like :RB3 that specifies what pin it is.
|
54
|
+
def def_pin(our_name, datasheet_name)
|
55
|
+
our_name = our_name.to_sym
|
56
|
+
@pin_aliases[our_name] = datasheet_name.to_sym
|
57
|
+
|
58
|
+
self::Shortcuts.send(:define_method, our_name) { pin our_name }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Define a RAM variable.
|
62
|
+
# @param name [Symbol] Specifies what you would like to call the variable.
|
63
|
+
# A method with this name will be added to your class's +Shortcuts+ module so it
|
64
|
+
# is available as a method on instances of your class and also in your RSpec tests.
|
65
|
+
# The method will return a {Variable} object that you can use to read or write the
|
66
|
+
# value of the actual variable in the simulation.
|
67
|
+
# @param type [Symbol] Specifies how to interpret the data in the variable and its size.
|
68
|
+
# For integers, it should be one of +:u8+, +:s8+, +:u16+, +:s16+, +:u24+, +:s24+, +:u32+, or +:s32+.
|
69
|
+
# The +s+ stands for signed and the +u+ stands for unsigned, and the number stands for the number
|
70
|
+
# of bits. All multi-byte integers are considered to be little Endian.
|
71
|
+
# @param opts [Hash] Specifies additional options. The options are:
|
72
|
+
# * +:symbol+: By default, we look for a symbol with the same name as the variable and
|
73
|
+
# use that as the location of the variable. This option lets you specify a different
|
74
|
+
# symbol to look for in the firmware, so you could call the variable one thing in your
|
75
|
+
# firmware and call it a different thing in your tests.
|
76
|
+
# This option is ignored if +:address is specified.
|
77
|
+
# * +:address+: An integer to use as the address of the variable.
|
78
|
+
def def_var(name, type, opts={})
|
79
|
+
allowed_keys = [:symbol]
|
80
|
+
invalid_keys = opts.keys - allowed_keys
|
81
|
+
if !invalid_keys.empty?
|
82
|
+
raise ArgumentError, "Unrecognized options: #{invalid_keys.join(", ")}"
|
83
|
+
end
|
84
|
+
|
85
|
+
name = name.to_sym
|
86
|
+
|
87
|
+
if opts[:address]
|
88
|
+
address = opts[:address].to_i
|
89
|
+
else
|
90
|
+
symbol = (opts[:symbol] || name).to_sym
|
91
|
+
if symbol.to_s.include?('@')
|
92
|
+
raise "Limitations in Microchip's code prevent us from accessing " +
|
93
|
+
"variables with '@' in the name like '#{symbol}'"
|
94
|
+
end
|
95
|
+
address = @var_address[symbol] or raise "Cannot find variable named '#{symbol}'."
|
96
|
+
end
|
97
|
+
|
98
|
+
klass = case type
|
99
|
+
when Class then type
|
100
|
+
when :u8 then VariableU8
|
101
|
+
when :s8 then VariableS8
|
102
|
+
when :u16 then VariableU16
|
103
|
+
when :s16 then VariableS16
|
104
|
+
when :u24 then VariableU24
|
105
|
+
when :s24 then VariableS24
|
106
|
+
when :u32 then VariableU32
|
107
|
+
when :s32 then VariableS32
|
108
|
+
else raise "Unknown type '#{type}'."
|
109
|
+
end
|
110
|
+
|
111
|
+
variable = klass.new(name, address)
|
112
|
+
variable.addresses.each do |address|
|
113
|
+
if @vars_by_address[address]
|
114
|
+
raise "Variable %s overlaps with %s at 0x%x" %
|
115
|
+
[variable, @vars_by_address[address], address]
|
116
|
+
end
|
117
|
+
@vars_by_address[address] = variable
|
118
|
+
end
|
119
|
+
@vars[name] = variable
|
120
|
+
|
121
|
+
self::Shortcuts.send(:define_method, name) { var name }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Define a flash (program memory or user ID) variable.
|
125
|
+
# @param name [Symbol] Specifies what you would like to call the variable.
|
126
|
+
# A method with this name will be added to your class's +Shortcuts+ module so it
|
127
|
+
# is available as a method on instances of your class and also in your RSpec tests.
|
128
|
+
# The method will return a {Variable} object that you can use to read or write the
|
129
|
+
# value of the actual variable in the simulation.
|
130
|
+
# @param type [Symbol] Specifies how to interpret the data in the variable and its size.
|
131
|
+
# The only supported option current is +:word+, which represents a full word of flash.
|
132
|
+
# @param opts [Hash] Specifies additional options. The options are:
|
133
|
+
# * +:symbol+: By default, we look for a symbol with the same name as the variable and
|
134
|
+
# use that as the location of the variable. This option lets you specify a different
|
135
|
+
# symbol to look for in the firmware.
|
136
|
+
# * +:address+: An integer to use as the address of the variable.
|
137
|
+
def def_flash_var(name, type, opts={})
|
138
|
+
allowed_keys = [:symbol, :address]
|
139
|
+
invalid_keys = opts.keys - allowed_keys
|
140
|
+
if !invalid_keys.empty?
|
141
|
+
raise ArgumentError, "Unrecognized options: #{invalid_keys.join(", ")}"
|
142
|
+
end
|
143
|
+
|
144
|
+
name = name.to_sym
|
145
|
+
|
146
|
+
if opts[:address]
|
147
|
+
address = opts[:address].to_i
|
148
|
+
else
|
149
|
+
symbol = (opts[:symbol] || name).to_sym
|
150
|
+
if symbol.to_s.include?('@')
|
151
|
+
raise "Limitations in Microchip's code prevent us from accessing " +
|
152
|
+
"variables with '@' in the name like '#{symbol}'"
|
153
|
+
end
|
154
|
+
label = labels[symbol] or raise "Could not find label named '#{symbol}'."
|
155
|
+
address = label.address
|
156
|
+
end
|
157
|
+
|
158
|
+
klass = case type
|
159
|
+
when Class then type
|
160
|
+
when :word then VariableWord
|
161
|
+
else raise "Unknown type '#{type}'."
|
162
|
+
end
|
163
|
+
|
164
|
+
variable = klass.new(name, address)
|
165
|
+
variable.max_value = @code_word_max_value
|
166
|
+
@flash_vars[name] = variable
|
167
|
+
|
168
|
+
self::Shortcuts.send(:define_method, name) { flash_var name }
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
# These are class methods that you can call on subclasses of {Sim}.
|
174
|
+
module ClassMethods
|
175
|
+
# A string like "PIC10F322" specifying the PIC device number.
|
176
|
+
attr_reader :device
|
177
|
+
|
178
|
+
# The path to a COF file for the PIC firmware, which was originally passed
|
179
|
+
# to the constructor.
|
180
|
+
attr_reader :filename
|
181
|
+
|
182
|
+
# A hash that associates our names for pins (like :main_output_pin) to datasheet
|
183
|
+
# pin names (like :RB3). These aliases are defined by {ClassDefinitionMethods#def_pin}.
|
184
|
+
attr_reader :pin_aliases
|
185
|
+
|
186
|
+
# A hash that associates RAM variable names to (unbound) {Variable} objects.
|
187
|
+
# The variables are defined by {ClassDefinitionMethods#def_var}.
|
188
|
+
attr_reader :vars
|
189
|
+
|
190
|
+
# A hash that associates flash variable names to (unbound) {Variable} objects.
|
191
|
+
# The variables are defined by {ClassDefinitionMethods#def_flash_var}.
|
192
|
+
attr_reader :flash_vars
|
193
|
+
|
194
|
+
# A hash that associates label names as symbols to {Label} objects.
|
195
|
+
attr_reader :labels
|
196
|
+
|
197
|
+
# The {ProgramFile} object representing the firmware.
|
198
|
+
attr_reader :program_file
|
199
|
+
|
200
|
+
private
|
201
|
+
# This gets called when a new subclass of PicSim is created.
|
202
|
+
def inherited(subclass)
|
203
|
+
subclass.instance_eval do
|
204
|
+
@pin_aliases = {}
|
205
|
+
@vars = {}
|
206
|
+
@vars_by_address = {}
|
207
|
+
@flash_vars = {}
|
208
|
+
const_set :Shortcuts, Module.new
|
209
|
+
include self::Shortcuts
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
def initialize_symbols
|
215
|
+
@program_file = ProgramFile.new(@filename, @device)
|
216
|
+
@var_address = program_file.var_addresses
|
217
|
+
@labels = program_file.labels
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
# This module is used in RPicSim's {file:RSpecIntegration.md RSpec integration}
|
223
|
+
# in order to let you call basic methods on the {RPicSim::Sim} object without having
|
224
|
+
# to prefix them with anything.
|
225
|
+
module BasicShortcuts
|
226
|
+
# This is the complete list of the basic shortcuts.
|
227
|
+
# You can call any of these methods by simply writing its name along with any arguments
|
228
|
+
# in an RSpec example.
|
229
|
+
#
|
230
|
+
# For example, these shortcuts allow you to just write +cycle_count+
|
231
|
+
# instead of +sim.cycle_count+.
|
232
|
+
ForwardedMethods = [
|
233
|
+
:cycle_count,
|
234
|
+
:every_step,
|
235
|
+
:flash_var,
|
236
|
+
:goto,
|
237
|
+
:label,
|
238
|
+
:location_address,
|
239
|
+
:nmmr,
|
240
|
+
:pc,
|
241
|
+
:pc_description,
|
242
|
+
:pin,
|
243
|
+
:ram_watcher,
|
244
|
+
:run_cycles,
|
245
|
+
:run_steps,
|
246
|
+
:run_subroutine,
|
247
|
+
:run_to,
|
248
|
+
:run_to_cycle_count,
|
249
|
+
:sfr,
|
250
|
+
:sfr_or_nmmr,
|
251
|
+
:step,
|
252
|
+
:var,
|
253
|
+
:wreg,
|
254
|
+
:stkptr,
|
255
|
+
]
|
256
|
+
|
257
|
+
extend Forwardable
|
258
|
+
def_delegators :@sim, *ForwardedMethods
|
259
|
+
end
|
260
|
+
|
261
|
+
extend ClassDefinitionMethods, ClassMethods
|
262
|
+
|
263
|
+
# Gets the program counter, an object that lets you read and write the
|
264
|
+
# current address in program space that is being executed.
|
265
|
+
# @return [RPicSim::ProgramCounter]
|
266
|
+
attr_reader :pc
|
267
|
+
|
268
|
+
# Returns a {MemoryWatcher} object configured to watch for changes to RAM.
|
269
|
+
# @return [MemoryWatcher]
|
270
|
+
attr_reader :ram_watcher
|
271
|
+
|
272
|
+
# Returns a {Register} object corresponding to WREG. You can use this
|
273
|
+
# to read and write the value of the W register.
|
274
|
+
# @return [Register]
|
275
|
+
attr_reader :wreg
|
276
|
+
|
277
|
+
# Returns a {Register} object corresponding to the stack pointer. You can use
|
278
|
+
# this to read and write the value of the stack pointer.
|
279
|
+
# @return [Register]
|
280
|
+
attr_reader :stkptr
|
281
|
+
|
282
|
+
# Returns a string like "PIC10F322" specifying the PIC device number.
|
283
|
+
# @return [String]
|
284
|
+
def device; self.class.device; end
|
285
|
+
|
286
|
+
# Returns the path to the firmware file.
|
287
|
+
# @return [String]
|
288
|
+
def filename; self.class.filename; end
|
289
|
+
|
290
|
+
# Makes a new simulation using the settings specified when the class was defined.
|
291
|
+
def initialize
|
292
|
+
@assembly = Mplab::MplabAssembly.new(device)
|
293
|
+
@assembly.start_simulator_and_debugger(filename)
|
294
|
+
@simulator = @assembly.simulator
|
295
|
+
@processor = @simulator.processor
|
296
|
+
|
297
|
+
# Set up our stores and helper objects.
|
298
|
+
@fr_memory = Memory.new @simulator.fr_memory
|
299
|
+
@sfr_memory = Memory.new @simulator.sfr_memory
|
300
|
+
@nmmr_memory = Memory.new @simulator.nmmr_memory
|
301
|
+
@program_memory = Memory.new @simulator.program_memory
|
302
|
+
@stack_memory = Memory.new @simulator.stack_memory
|
303
|
+
@test_memory = Memory.new @simulator.test_memory
|
304
|
+
|
305
|
+
@pc = ProgramCounter.new(@simulator.processor)
|
306
|
+
|
307
|
+
@step_callbacks = []
|
308
|
+
|
309
|
+
initialize_pins
|
310
|
+
initialize_sfrs_and_nmmrs
|
311
|
+
initialize_vars
|
312
|
+
initialize_flash_vars
|
313
|
+
|
314
|
+
@ram_watcher = MemoryWatcher.new(self, @simulator.fr_memory, @vars.values + @sfrs.values)
|
315
|
+
end
|
316
|
+
|
317
|
+
private
|
318
|
+
|
319
|
+
def initialize_pins
|
320
|
+
pins = @simulator.pins.collect { |mplab_pin| Pin.new(mplab_pin) }
|
321
|
+
|
322
|
+
#pins.reject! { |p| p.to_s == "VDD" } or raise "Failed to filter out VDD pin."
|
323
|
+
#pins.reject! { |p| p.to_s == "VSS" } or raise "Failed to filter out VSS pin."
|
324
|
+
|
325
|
+
@pins_by_name = {}
|
326
|
+
pins.each do |pin|
|
327
|
+
pin.names.each do |name|
|
328
|
+
@pins_by_name[name.to_sym] = pin
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
self.class.pin_aliases.each do |our_name, datasheet_name|
|
333
|
+
@pins_by_name[our_name] = @pins_by_name[datasheet_name] or raise "Pin #{datasheet_name} not found."
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def initialize_vars
|
338
|
+
@vars = {}
|
339
|
+
self.class.vars.each do |name, unbound_var|
|
340
|
+
@vars[name] = unbound_var.bind(@fr_memory)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def initialize_flash_vars
|
345
|
+
@flash_vars = {}
|
346
|
+
memories = [@program_memory, @test_memory]
|
347
|
+
self.class.flash_vars.each do |name, unbound_var|
|
348
|
+
possible_memories = memories.select { |m| m.is_valid_address?(unbound_var.address) }
|
349
|
+
if possible_memories.empty?
|
350
|
+
raise "Flash variable has an invalid address: #{unbound_var.inspect}"
|
351
|
+
elsif possible_memories.size > 1
|
352
|
+
raise "Flash variable's address is valid in both program memory and test memory. Not sure which memory to use: #{unbound_var.inspect}."
|
353
|
+
end
|
354
|
+
|
355
|
+
@flash_vars[name] = unbound_var.bind(possible_memories.first)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def initialize_sfrs_and_nmmrs
|
360
|
+
@sfrs = {}
|
361
|
+
@assembly.device_info.sfrs.each do |sfr|
|
362
|
+
@sfrs[sfr.name.to_sym] = Register.new @processor.get_sfr(sfr.name), @sfr_memory, sfr.width
|
363
|
+
end
|
364
|
+
|
365
|
+
@nmmrs = {}
|
366
|
+
@assembly.device_info.nmmrs.each do |nmmr|
|
367
|
+
@nmmrs[nmmr.name.to_sym] = Register.new @processor.get_nmmr(nmmr.name), @nmmr_memory, nmmr.width
|
368
|
+
end
|
369
|
+
|
370
|
+
@wreg = sfr_or_nmmr(:WREG)
|
371
|
+
@stkptr = sfr_or_nmmr(:STKPTR)
|
372
|
+
end
|
373
|
+
|
374
|
+
public
|
375
|
+
|
376
|
+
# Returns a Pin object if a pin by that name is found, or raises an exception.
|
377
|
+
# @param name [Symbol] The name from the datasheet or a name specified in a
|
378
|
+
# call to {ClassDefinitionMethods#def_pin} in the class definition.
|
379
|
+
# @return [Pin]
|
380
|
+
def pin(name)
|
381
|
+
@pins_by_name[name.to_sym] or raise ArgumentError, "Cannot find pin named '#{name}'."
|
382
|
+
end
|
383
|
+
|
384
|
+
# Returns a {Register} object if an SFR by that name is found,
|
385
|
+
# or raises an exception.
|
386
|
+
# @param name [Symbol] The name from the datasheet.
|
387
|
+
# @return [Register]
|
388
|
+
def sfr(name)
|
389
|
+
@sfrs[name.to_sym] or raise ArgumentError, "Cannot find SFR named '#{name}'."
|
390
|
+
end
|
391
|
+
|
392
|
+
# Returns a {Register} object if an SFR or NMMR by that name is found,
|
393
|
+
# or raises an exception.
|
394
|
+
# @param name [Symbol] The name from the datasheet.
|
395
|
+
# @return [Register]
|
396
|
+
def sfr_or_nmmr(name)
|
397
|
+
name = name.to_sym
|
398
|
+
@sfrs[name] || @nmmrs[name] or raise ArgumentError, "Cannot find SFR or NMMR named '#{name}'."
|
399
|
+
end
|
400
|
+
|
401
|
+
# Returns a {Register} object if an NMMR by that name is found,
|
402
|
+
# or raises an exception.
|
403
|
+
# @param name [Symbol] The name from the datasheet.
|
404
|
+
# @return [Register]
|
405
|
+
def nmmr(name)
|
406
|
+
@nmmrs[name.to_sym] or raise ArgumentError, "Cannot find NMMR named '#{name}'."
|
407
|
+
end
|
408
|
+
|
409
|
+
# Returns a {Variable} object if a RAM variable by that name is found,
|
410
|
+
# or raises an exception.
|
411
|
+
# @return [Variable]
|
412
|
+
def var(name)
|
413
|
+
@vars[name.to_sym] or raise ArgumentError, "Cannot find var named '#{name}'."
|
414
|
+
end
|
415
|
+
|
416
|
+
# Returns a {Variable} object if a flash (program memory) variable by that name is found,
|
417
|
+
# or raises an exception.
|
418
|
+
# @return [Variable]
|
419
|
+
def flash_var(name)
|
420
|
+
@flash_vars[name.to_sym] or raise ArgumentError, "Cannot find flash var named '#{name}'."
|
421
|
+
end
|
422
|
+
|
423
|
+
# Returns a {Label} object if a program label by that name is found.
|
424
|
+
# The name is specified in the code that defined the label. If you are using a C compiler,
|
425
|
+
# you will probably need to prefix the name with an underscore.
|
426
|
+
# @return [Label]
|
427
|
+
def label(name)
|
428
|
+
self.class.program_file.label(name)
|
429
|
+
end
|
430
|
+
|
431
|
+
# Returns the number of instruction cycles simulated in this simulation.
|
432
|
+
# @return [Integer]
|
433
|
+
def cycle_count
|
434
|
+
@simulator.stopwatch_value
|
435
|
+
end
|
436
|
+
|
437
|
+
# Registers a new callback to be run after every simulation step.
|
438
|
+
# Each time the simulation takes a step, the provided block will be called.
|
439
|
+
def every_step(&proc)
|
440
|
+
@step_callbacks << proc
|
441
|
+
end
|
442
|
+
|
443
|
+
# Executes one more instruction.
|
444
|
+
# @return nil
|
445
|
+
def step
|
446
|
+
@assembly.debugger_step
|
447
|
+
@step_callbacks.each(&:call)
|
448
|
+
nil # To make using the ruby debugger more pleasant.
|
449
|
+
end
|
450
|
+
|
451
|
+
# Executes the specified number of instructions.
|
452
|
+
# @param step_count [Integer]
|
453
|
+
# @return nil
|
454
|
+
def run_steps(step_count)
|
455
|
+
step_count.times { step }
|
456
|
+
nil # To make using the ruby debugger more pleasant.
|
457
|
+
end
|
458
|
+
|
459
|
+
# Runs the simulation until one of the given conditions has been met, then
|
460
|
+
# stops and returns the condition that was met.
|
461
|
+
#
|
462
|
+
#
|
463
|
+
# Example usage in RSpec:
|
464
|
+
# result = run_to [:mylabel, :return], cycle_limit: 400
|
465
|
+
# result.should == :return
|
466
|
+
#
|
467
|
+
# @param conditions Each element of the conditions array should be
|
468
|
+
# a Proc that returns true when the condition is met, a symbol corresponding
|
469
|
+
# to a program label, or any other object that is a valid argument to
|
470
|
+
# {#convert_condition_to_proc}.
|
471
|
+
# If there is only one condition, you can pass it directly in as the first
|
472
|
+
# argument without wrapping it in an array.
|
473
|
+
# @param opts [Hash] A hash of options.
|
474
|
+
# - +cycle_limit+: The maximum number of cycles to run, as an integer.
|
475
|
+
# It is recommended to always specify this to avoid accidentally
|
476
|
+
# making an infinite loop. Note that multi-cycle instructions mean
|
477
|
+
# that this limit will sometimes be violated by one cycle.
|
478
|
+
# If none of the conditions are met by the cycle limit, an exception is raised.
|
479
|
+
# - +cycles+: A range of integers specifying how long you expect
|
480
|
+
# it to take to reach one of the conditions, for example e.g. +1000..2000+.
|
481
|
+
# If a condition is met before the minimum, an exception is raised.
|
482
|
+
# If none of the conditions are met after the maximum, an exception is
|
483
|
+
# raised.
|
484
|
+
#
|
485
|
+
# This option is a more powerful version of +cycle_limit+, so it cannot
|
486
|
+
# be used at the same time as +cycle_limit+.
|
487
|
+
# @return The condition that was met which caused the run to stop.
|
488
|
+
def run_to(conditions, opts={})
|
489
|
+
conditions = Array(conditions)
|
490
|
+
if conditions.empty?
|
491
|
+
raise ArgumentError, "Must specify at least one condition."
|
492
|
+
end
|
493
|
+
|
494
|
+
condition_procs = conditions.collect &method(:convert_condition_to_proc)
|
495
|
+
|
496
|
+
allowed_keys = [:cycle_limit, :cycles]
|
497
|
+
invalid_keys = opts.keys - allowed_keys
|
498
|
+
if !invalid_keys.empty?
|
499
|
+
raise ArgumentError, "Unrecognized options: #{invalid_keys.join(", ")}"
|
500
|
+
end
|
501
|
+
|
502
|
+
if opts[:cycles] && opts[:cycle_limit]
|
503
|
+
raise ArgumentError, "Cannot specify both :cycles and :cycle_limit."
|
504
|
+
end
|
505
|
+
|
506
|
+
start_cycle = cycle_count
|
507
|
+
if opts[:cycles]
|
508
|
+
raise "Invalid range: #{opts[:cycles].inspect}." unless opts[:cycles].min && opts[:cycles].max
|
509
|
+
min_cycle = start_cycle + opts[:cycles].min
|
510
|
+
max_cycle = start_cycle + opts[:cycles].max
|
511
|
+
max_cycle -= 1 if opts[:cycles].exclude_end?
|
512
|
+
elsif opts[:cycle_limit]
|
513
|
+
max_cycle = start_cycle + opts[:cycle_limit] if opts[:cycle_limit]
|
514
|
+
end
|
515
|
+
|
516
|
+
# Loop as long as none of the conditions are satisfied.
|
517
|
+
while !(met_condition_index = condition_procs.find_index &:call)
|
518
|
+
if max_cycle && cycle_count >= max_cycle
|
519
|
+
raise "Failed to reach #{conditions.inspect} after #{cycle_count - start_cycle} cycles."
|
520
|
+
end
|
521
|
+
|
522
|
+
step
|
523
|
+
end
|
524
|
+
|
525
|
+
met_condition = conditions[met_condition_index]
|
526
|
+
|
527
|
+
if min_cycle && cycle_count < min_cycle
|
528
|
+
raise "Reached #{met_condition.inspect} in only #{cycle_count - start_cycle} cycles " +
|
529
|
+
"but expected it to take at least #{min_cycle - start_cycle}."
|
530
|
+
end
|
531
|
+
|
532
|
+
# Return the argument that specified the condition that was satisfied.
|
533
|
+
return met_condition
|
534
|
+
end
|
535
|
+
|
536
|
+
# Gets the address of the specified location in program memory.
|
537
|
+
# This is a helper for processing the main argument to {#goto} and {#run_subroutine}.
|
538
|
+
# @param location One of the following:
|
539
|
+
# - The name of a program label, as a symbol or string.
|
540
|
+
# - A {Label} object.
|
541
|
+
# - An integer representing the address.
|
542
|
+
# @return [Integer]
|
543
|
+
def location_address(location)
|
544
|
+
case location
|
545
|
+
when Integer then location
|
546
|
+
when Label then location.address
|
547
|
+
when Symbol, String then label(location).address
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
# Changes the {#pc} value to be equal to the address of the given location.
|
552
|
+
# @param location Any valid argument to {#location_address}.
|
553
|
+
def goto(location)
|
554
|
+
pc.value = location_address(location)
|
555
|
+
end
|
556
|
+
|
557
|
+
# Runs the subroutine at the given location. This can be useful for doing
|
558
|
+
# unit tests of subroutines in your firmware.
|
559
|
+
#
|
560
|
+
# The current program counter value will be pushed onto the stack before
|
561
|
+
# running the subroutine so that after the subroutine is done the simulation
|
562
|
+
# can proceed as it was before.
|
563
|
+
#
|
564
|
+
# Example usage in RSpec:
|
565
|
+
# run_subroutine :calculateSum, cycle_limit: 20
|
566
|
+
# sum.value.should == 30
|
567
|
+
#
|
568
|
+
# @param location Any valid argument to {#location_address}. It should
|
569
|
+
# generally point to a subroutine in program memory that will end by
|
570
|
+
# executing a return instructions.
|
571
|
+
# @param opts Any of the options supported by {#run_to}.
|
572
|
+
def run_subroutine(location, opts={})
|
573
|
+
stack_push pc.value
|
574
|
+
goto location
|
575
|
+
run_to :return, opts
|
576
|
+
end
|
577
|
+
|
578
|
+
# Runs the simulation for the given number of instruction cycles.
|
579
|
+
# Note that the existence of multi-cycle instructions means that sometimes this
|
580
|
+
# method can run one cycle longer than desired.
|
581
|
+
# @param num_cycles [Integer]
|
582
|
+
def run_cycles(num_cycles)
|
583
|
+
run_to_cycle_count cycle_count + num_cycles
|
584
|
+
end
|
585
|
+
|
586
|
+
# Runs the simulation until the {#cycle_count} is greater than or equal to the
|
587
|
+
# given cycle count.
|
588
|
+
# @param count [Integer]
|
589
|
+
def run_to_cycle_count(count)
|
590
|
+
while cycle_count < count
|
591
|
+
step
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
# Simulates a return instruction being executed by popping the top value off
|
596
|
+
# of the stack and setting the {#pc} value equal to it.
|
597
|
+
# This can be useful for speeding up your tests when you have a very slow function
|
598
|
+
# and just want to skip it.
|
599
|
+
def return
|
600
|
+
if stkptr.value == 0
|
601
|
+
raise "Cannot return because stack pointer is 0."
|
602
|
+
end
|
603
|
+
|
604
|
+
# Simulate popping the stack.
|
605
|
+
stkptr.value -= 1
|
606
|
+
pc.value = @stack_memory.read_word(stkptr.value)
|
607
|
+
end
|
608
|
+
|
609
|
+
# Generates a friendly human-readable string description of where the
|
610
|
+
# program counter is currently using the symbol table.
|
611
|
+
def pc_description
|
612
|
+
self.class.program_file.address_description(pc.value)
|
613
|
+
end
|
614
|
+
|
615
|
+
# Pushes the given address onto the simulated call stack.
|
616
|
+
def stack_push(value)
|
617
|
+
if !@stack_memory.is_valid_address?(stkptr.value)
|
618
|
+
raise "Simulated stack is full (stack pointer = #{stkptr.value})."
|
619
|
+
end
|
620
|
+
|
621
|
+
@stack_memory.write_word(stkptr.value, value)
|
622
|
+
stkptr.value += 1
|
623
|
+
end
|
624
|
+
|
625
|
+
# Gets the contents of the stack as an array of integers.
|
626
|
+
# @return [Array(Integer)] An array of integers.
|
627
|
+
def stack_contents
|
628
|
+
(0...stkptr.value).collect do |n|
|
629
|
+
@stack_memory.read_word(n)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
# Returns a call stack trace representing the current state of the
|
634
|
+
# simulation. Printing this stack trace can help you figure out what part
|
635
|
+
# of your code is running and why.
|
636
|
+
# @return [StackTrace]
|
637
|
+
def stack_trace
|
638
|
+
# The stack stores return addresses, not call addresses.
|
639
|
+
# We get the call addresses by calling pred (subtract 1).
|
640
|
+
# TODO: make this work for PIC18 devices where we probably have to subtract 2
|
641
|
+
addresses = stack_contents.collect(&:pred) + [pc.value]
|
642
|
+
entries = addresses.collect do |address|
|
643
|
+
StackTraceEntry.new address, self.class.program_file.address_description(address)
|
644
|
+
end
|
645
|
+
StackTrace.new(entries)
|
646
|
+
end
|
647
|
+
|
648
|
+
def inspect
|
649
|
+
"#<#{self.class}:0x%x, #{pc_description}, stkptr = #{stkptr.value}>" % object_id
|
650
|
+
end
|
651
|
+
|
652
|
+
# Converts the specified condition into a Proc that, when called, will return a
|
653
|
+
# truthy value if the condition is satisfied.
|
654
|
+
# This is a helper for processing the main argument to {#run_to}.
|
655
|
+
# @param c One of the following:
|
656
|
+
# - The symbol +:return+.
|
657
|
+
# The condition will be true if the current subroutine has returned.
|
658
|
+
# This is implemented by looking to see whether the stack pointer has
|
659
|
+
# decreased one level below the level it was at when this method was called.
|
660
|
+
# - The name of a program label, as a symbol or string, or a
|
661
|
+
# {Label} object. The condition will be true if the {#pc}
|
662
|
+
# value is equal to the label address.
|
663
|
+
# - An integer representing an address. The condition will be true if the
|
664
|
+
# {#pc} value is equal to the address.
|
665
|
+
# - A Proc. The Proc will be returned unchanged.
|
666
|
+
# @return [Integer]
|
667
|
+
def convert_condition_to_proc(c)
|
668
|
+
case c
|
669
|
+
|
670
|
+
when Proc
|
671
|
+
c
|
672
|
+
|
673
|
+
when Integer
|
674
|
+
Proc.new { pc.value == c }
|
675
|
+
|
676
|
+
when :return
|
677
|
+
current_val = stkptr.value
|
678
|
+
if current_val == 0
|
679
|
+
raise "The stack pointer is 0; waiting for a return would be strange and might not work."
|
680
|
+
else
|
681
|
+
target_val = current_val - 1
|
682
|
+
end
|
683
|
+
Proc.new { stkptr.value == target_val }
|
684
|
+
|
685
|
+
when Label
|
686
|
+
convert_condition_to_proc c.address
|
687
|
+
|
688
|
+
when String, Symbol
|
689
|
+
convert_condition_to_proc label(c).address
|
690
|
+
|
691
|
+
else
|
692
|
+
raise ArgumentError, "Invalid run-termination condition #{c.inspect}"
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
def shortcuts
|
697
|
+
self.class::Shortcuts
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
end
|
702
|
+
|