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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +36 -0
  3. data/Gemfile +10 -0
  4. data/Introduction.md +64 -0
  5. data/LICENSE.txt +24 -0
  6. data/README.md +66 -0
  7. data/docs/ChangeLog.md +10 -0
  8. data/docs/Contributing.md +30 -0
  9. data/docs/Debugging.md +96 -0
  10. data/docs/DefiningSimulationClass.md +84 -0
  11. data/docs/DesignDecisions.md +48 -0
  12. data/docs/HowMPLABXIsFound.md +14 -0
  13. data/docs/IntegrationTesting.md +15 -0
  14. data/docs/IntroductionToRSpec.md +203 -0
  15. data/docs/IntroductionToRuby.md +90 -0
  16. data/docs/KnownIssues.md +204 -0
  17. data/docs/Labels.md +39 -0
  18. data/docs/MakingTestsRunFaster.md +29 -0
  19. data/docs/Manual.md +38 -0
  20. data/docs/PersistentExpectations.md +100 -0
  21. data/docs/Pins.md +143 -0
  22. data/docs/PreventingCallStackOverflow.md +94 -0
  23. data/docs/QuickStartGuide.md +85 -0
  24. data/docs/RSpecIntegration.md +119 -0
  25. data/docs/RamWatcher.md +46 -0
  26. data/docs/Running.md +129 -0
  27. data/docs/SFRs.md +71 -0
  28. data/docs/Stubbing.md +161 -0
  29. data/docs/SupportedCompilers.md +5 -0
  30. data/docs/SupportedDevices.md +14 -0
  31. data/docs/SupportedMPLABXVersions.md +12 -0
  32. data/docs/SupportedOperatingSystems.md +3 -0
  33. data/docs/UnitTesting.md +9 -0
  34. data/docs/Variables.md +140 -0
  35. data/lib/rpicsim.rb +15 -0
  36. data/lib/rpicsim/call_stack_info.rb +306 -0
  37. data/lib/rpicsim/flaws.rb +76 -0
  38. data/lib/rpicsim/instruction.rb +178 -0
  39. data/lib/rpicsim/label.rb +28 -0
  40. data/lib/rpicsim/memory.rb +29 -0
  41. data/lib/rpicsim/memory_watcher.rb +98 -0
  42. data/lib/rpicsim/mplab.rb +138 -0
  43. data/lib/rpicsim/mplab/mplab_assembly.rb +102 -0
  44. data/lib/rpicsim/mplab/mplab_device_info.rb +40 -0
  45. data/lib/rpicsim/mplab/mplab_disassembler.rb +23 -0
  46. data/lib/rpicsim/mplab/mplab_instruction.rb +32 -0
  47. data/lib/rpicsim/mplab/mplab_memory.rb +33 -0
  48. data/lib/rpicsim/mplab/mplab_nmmr_info.rb +21 -0
  49. data/lib/rpicsim/mplab/mplab_observer.rb +12 -0
  50. data/lib/rpicsim/mplab/mplab_pin.rb +61 -0
  51. data/lib/rpicsim/mplab/mplab_processor.rb +30 -0
  52. data/lib/rpicsim/mplab/mplab_program_file.rb +35 -0
  53. data/lib/rpicsim/mplab/mplab_register.rb +25 -0
  54. data/lib/rpicsim/mplab/mplab_sfr_info.rb +21 -0
  55. data/lib/rpicsim/mplab/mplab_simulator.rb +102 -0
  56. data/lib/rpicsim/pin.rb +61 -0
  57. data/lib/rpicsim/program_counter.rb +19 -0
  58. data/lib/rpicsim/program_file.rb +160 -0
  59. data/lib/rpicsim/register.rb +78 -0
  60. data/lib/rpicsim/rspec.rb +11 -0
  61. data/lib/rpicsim/rspec/be_predicate.rb +12 -0
  62. data/lib/rpicsim/rspec/helpers.rb +48 -0
  63. data/lib/rpicsim/rspec/persistent_expectations.rb +53 -0
  64. data/lib/rpicsim/rspec/sim_diagnostics.rb +51 -0
  65. data/lib/rpicsim/search.rb +20 -0
  66. data/lib/rpicsim/sim.rb +702 -0
  67. data/lib/rpicsim/stack_trace.rb +32 -0
  68. data/lib/rpicsim/variable.rb +236 -0
  69. data/lib/rpicsim/version.rb +3 -0
  70. 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
@@ -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
+