rpicsim 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+