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
data/docs/Stubbing.md ADDED
@@ -0,0 +1,161 @@
1
+ Stubbing
2
+ ====
3
+
4
+ Stubbing is a technique used in {file:UnitTesting.md unit testing} to replace a routine in a system under test with a simpler routine whose behavior can be specified in the test.
5
+ Stubbing allows you to limit the amount of code you are testing, which can make that test easier to design and faster to run.
6
+ RPicSim does not have any special features to support stubbing, but it can easily be accomplished using features already present.
7
+
8
+
9
+ A basic stub
10
+ ----
11
+
12
+ To stub a method in the most basic way, you can do something like this:
13
+
14
+ !!!ruby
15
+ every_step do
16
+ if pc.value == label(:foo).address
17
+ sim.return
18
+ end
19
+ end
20
+
21
+ The example above just alters our simulation so that whenever the `foo` subroutine is called, instead of running as normal it will return immediately using {RPicSim::Sim#return}.
22
+
23
+ The example above can be expanded in many ways:
24
+ You might read and write from {file:Variables.md variables} and {file:SFRs.md SFRs}.
25
+ You might record information about how the subroutine was called.
26
+
27
+
28
+ A stub that counts
29
+ ----
30
+
31
+ If you want to know how many times the stubbed routine is called, you could do this:
32
+
33
+ !!!ruby
34
+ @foo_count = 0
35
+ every_step do
36
+ if pc.value == label(:foo).address
37
+ @foo_count += 1
38
+ sim.return
39
+ end
40
+ end
41
+
42
+ Using a Ruby instance variable `@foo_count` instead of a simple local variable means that this code could go in a before hook and the code that checks the count could be in the main part of the RSpec example.
43
+
44
+
45
+ A stub that records parameters
46
+ ----
47
+
48
+ You might want to test that the right parameters are getting supplied to the stubbed routine.
49
+ To capture information about the stubbed routine's parameters or anything else about the state of the simulation, you could use a Ruby array:
50
+
51
+ !!!ruby
52
+ @foo_calls = []
53
+ every_step do
54
+ if pc.value == label(:foo).address
55
+ @foo_calls << { a: foo_param_a.value, b: foo_param_b.value }
56
+ sim.return
57
+ end
58
+ end
59
+
60
+ In your RSpec examples, you can test that the routine was called the right number of times and with the expected parameters:
61
+
62
+ !!!ruby
63
+ expect(@foo_calls).to eq [ {a: 1, b: 25}, {a: 2, b: 24 } ]
64
+
65
+
66
+ LongDelay example
67
+ ----
68
+
69
+ This is a more complete example showing how to make a simple stub that counts the number of times it was called.
70
+
71
+ Here is a minimal MPASM assembly program with two routines.
72
+ The `bigDelay` routine delays for a long time using a 16-bit counter and a loop.
73
+ The `cooldown` routine either calls `bigDelay` once or twice depending on some condition.
74
+
75
+ !!!plain
76
+ #include p10F322.inc
77
+ __config(0x3E06)
78
+ udata
79
+ hot res 1
80
+ counter res 2
81
+ code 0
82
+
83
+ cooldown:
84
+ btfsc hot, 0
85
+ call bigDelay
86
+ call bigDelay
87
+ return
88
+
89
+ bigDelay:
90
+ movlw 255
91
+ movwf counter
92
+ movlw 255
93
+ movwf counter + 1
94
+ delayLoop:
95
+ decfsz counter, F
96
+ goto delayLoop
97
+ decfsz counter+1, F
98
+ goto delayLoop
99
+ return
100
+ end
101
+
102
+ Suppose we want to write a unit test for the logic in the `cooldown` method.
103
+ We could just run the subroutine in various conditions and see how long it takes to finish.
104
+ However, that test could be very slow to run.
105
+ Instead, we should stub the `bigDelay` method and make the stub count how many times it was called.
106
+
107
+ In `spec/spec_helper.rb`, we make a simulation class that points to the compiled COF file. There is nothing special here:
108
+
109
+ !!!ruby
110
+ require 'rpicsim/rspec'
111
+
112
+ class LongDelay < RPicSim::Sim
113
+ device_is "PIC10F322"
114
+ filename_is File.dirname(__FILE__) + "../firmware/dist/firmware.cof"
115
+ def_var :hot, :u8
116
+ end
117
+
118
+ In `spec/cooldown_spec.rb`, we stub the `bigDelay` routine and test `cooldown` to make sure it calls `bigDelay` the right number of times:
119
+
120
+ !!!ruby
121
+ require 'rpicsim/rspec'
122
+
123
+ describe "cooldown" do
124
+ before do
125
+ start_sim Firmware::LongDelay
126
+
127
+ # Stub the "bigDelay" function because it takes a long time to run.
128
+ # Also, count how many times it was called.
129
+ @big_delay_count = 0
130
+ every_step do
131
+ if pc.value == label(:bigDelay).address
132
+ @big_delay_count += 1
133
+ sim.return
134
+ end
135
+ end
136
+ end
137
+
138
+ context "when the room is cool" do
139
+ before do
140
+ hot.value = 0
141
+ end
142
+
143
+ it "only does one big delay" do
144
+ run_subroutine :cooldown, cycle_limit: 100
145
+ expect(@big_delay_count).to eq 1
146
+ end
147
+ end
148
+
149
+ context "when the room is hot" do
150
+ before do
151
+ hot.value = 1
152
+ end
153
+
154
+ it "does two big delays" do
155
+ run_subroutine :cooldown, cycle_limit: 100
156
+ expect(@big_delay_count).to eq 2
157
+ end
158
+ end
159
+ end
160
+
161
+ This makes our test much faster and allows us to just test the behavior of the `cooldown` routine.
@@ -0,0 +1,5 @@
1
+ # Supported compilers
2
+
3
+ RPicSim supports any compiler that can generate a standard Microchip COF file that is recognized by the COF-loading code in MPLAB X. If your compiler does not generate such a file, then you could use an Intel HEX file instead, but then RPicSim will not automatically have access to your firmware's symbol table, so it will not know where variables, functions, and labels are located.
4
+
5
+ RPicSim has been extensively tested with COF files produced by MPASM. At the time of this writing, it has not been tested with XC8 or C18.
@@ -0,0 +1,14 @@
1
+ # Supported devices
2
+
3
+ RPicSim aims to support all 8-bit PIC microcontrollers. No support is planned for 16-bit or 32-bit PIC microcontrollers because the author does not have an interest in using those devices. It might be easy to add support for them, but the code that handles {file:SFRs.md SFRs} would have to be adjusted to allow SFRs with more than 8 bits.
4
+
5
+ RPicSim relies on MPLAB X code to perform the actual simulation, and MPLAB X abstracts away most of the differences between PICs.
6
+
7
+ The 8-bit PICs have {http://www.microchip.com/pagehandler/en-us/family/8bit/architecture/home.html four different architectures}:
8
+
9
+ - Baseline
10
+ - Midrange
11
+ - Enhanced Midrange
12
+ - PIC18
13
+
14
+ There is currently some code in {RPicSim::ProgramFile#instruction} that only works with baseline and midrange PICs, but it should be easy to expand it to other PICs. This code is only used to calculate call stack depth information for users who want that feature; it is not used for actual simulations.
@@ -0,0 +1,12 @@
1
+ # Supported MPLAB X versions
2
+
3
+ RPicSim uses code in MPLAB X to actually perform the PIC simulation. Therefore, changes to MPLAB X might affect RPicSim and require changes. Currently, RPicSim supports the following versions of MPLAB X:
4
+
5
+ - 1.85
6
+ - 1.90
7
+ - 1.95
8
+ - 2.00
9
+
10
+ The different versions of MPLAB X have different problems that affect the simulation. For more information, see the {file:KnownIssues.md Known issues page}.
11
+
12
+ If you would like to power RPicSim with one version of MPLAB X and use another as your actual IDE, see {file:HowMPLABXIsFound.md How MPLAB X is found}.
@@ -0,0 +1,3 @@
1
+ # Supported Operating Systems
2
+
3
+ RPicSim currently only supports Windows. However, it should be easy to get it working on Linux or Mac OS X: probably the only code that would need to be changed is the code in `mplab_x.rb` that helps find the MPLAB X directory automatically and the code that detects what version of MPLAB X is being used.
@@ -0,0 +1,9 @@
1
+ Unit Testing
2
+ ====
3
+
4
+ A _unit test_ is a test for a relatively small piece of a code, which tests that code in isolation from the other code in the application.
5
+ Unit testing is simply the practice of writing and running units tests to go along with your code.
6
+
7
+ The {file:Running.md Running page} explains how to run small portions of your code using RPicSim.
8
+ RPicSim allows you to access {file:Variables.md variables} and {file:SFRs SFRs}, so you can put the simulation into the desired state before running the code and then test that the simulation is in the right state after the code has executed.
9
+ RPicSim can also be used for {file:Stubbing.md stubbing subroutines} so that you can simply test the behavior of one subroutine instead of all the subroutines it calls.
data/docs/Variables.md ADDED
@@ -0,0 +1,140 @@
1
+ Variables
2
+ ====
3
+
4
+ RPicSim allows you to read and write from simulated variables stored in RAM or flash, which can be useful for {file:UnitTesting.md unit testing}. Variables are represented as Ruby objects that are instances of a subclass of {RPicSim::Variable}.
5
+
6
+ To access a variable, RPicSim needs to know the name it will be called in your Ruby code, what data type it is (e.g. 16-bit unsigned integer), and its address in memory.
7
+ In most cases, RPicSim can deduce the address by looking at the symbol table in your COF file, so you will not need to
8
+ type the address.
9
+ However, RPicSim cannot deduce the data type of a variable, so any variables used need to be explicitly defined beforehand.
10
+
11
+ Defining a RAM variable
12
+ ----
13
+
14
+ RAM variables that you want to access from Ruby must be defined in the {file:DefiningSimulationClass.md simulation class} using {RPicSim::Sim::ClassDefinitionMethods#def_var def_var}. For example:
15
+
16
+ !!!ruby
17
+ class MySim < RPicSim::Sim
18
+ #...
19
+
20
+ def_var :counter, :u8
21
+
22
+ end
23
+
24
+ The first argument to `def_var` specifies what to call the variable in Ruby code. Using the example above, you could access the variable object by passing `:counter` as the argument to {RPicSim::Sim#var}:
25
+
26
+ !!!ruby
27
+ sim.var(:counter)
28
+
29
+ Each variable also has a "shortcut" method by the same name. This means that you can access the variable like this:
30
+
31
+ !!!ruby
32
+ sim.counter
33
+
34
+ The shortcuts are also available in RSpec thanks to RPicsim's {file:RSpecIntegration.md RSpec integration}, so you can simply write `counter` in any of your RSpec examples:
35
+
36
+ !!!ruby
37
+ it "drives the main output high" do
38
+ expect(counter.value).to eq 44
39
+ end
40
+
41
+ The second argument to `def_var` specifies the data type of the variable. This is required. For the full list of allowed types, see {RPicSim::Sim::ClassDefinitionMethods#def_var}.
42
+
43
+ In the example above, RPicSim will look in your firmware's COF file for a RAM symbol named "counter" and it will use that as the address for the variable, so you do not need to specify the address yourself.
44
+
45
+ You can use the `symbol` option to specify what symbol in the symbol table marks the location of the variable. For example:
46
+
47
+ !!!ruby
48
+ def_var :counter, :u8, symbol: :_counter
49
+
50
+ The example above shows how you could access a variable from a C compiler (which will generally be prefixed with an underscore) without having to type the underscore in your tests.
51
+ More generally, the `symbol` option allows you to call a variable one thing in your firmware and call it a different thing in your tests.
52
+
53
+ RPicSim will raise an exception if it cannot find the specified symbol in the symbol table. To troubleshoot this, you might print the list of variables that RPicSim found:
54
+
55
+ !!!ruby
56
+ p sim.class.program_file.var_addresses.keys
57
+
58
+ You can use the `address` option to specify an arbitrary address instead of using the symbol table. For example:
59
+
60
+ !!!ruby
61
+ def_var :counter, :u8, address: 0x63
62
+
63
+
64
+ Defining Flash variables
65
+ ----
66
+
67
+ Flash (program space) variables work the same way as RAM variables except:
68
+
69
+ * They are defined with {RPicSim::Sim::ClassDefinitionMethods#def_flash_var def_flash_var}.
70
+ * The set of allowed data types for the second argument of `def_flash_var` is different, and you can see the documentation by clicking the link above.
71
+ * Flash variables cannot be accessed with {RPicSim::Sim#var}, but can be accessed with {RPicSim::Sim#flash_var}
72
+
73
+
74
+ Using a variable
75
+ ----
76
+
77
+ Once you have defined a variable and accessed it using one of the methods above, you will have an instance of a subclass of {RPicSim::Variable}. You can read and write the value of the variable using the `value` attribute:
78
+
79
+ !!!ruby
80
+ counter.value = 0x6A
81
+ expect(counter.value).to eq 0x6A
82
+
83
+
84
+ Addition example
85
+ ----
86
+
87
+ This section contains a simple example showing how to apply the information above and use {RPicSim::Variable} objects.
88
+
89
+ Here is a minimal MPASM assembly program for the PIC10F322 that does not actually do anything but it has a 16-bit addition subroutine:
90
+
91
+ !!!plain
92
+ #include p10F322.inc
93
+ __config(0x3E06)
94
+ udata
95
+ x res 2
96
+ y res 2
97
+ z res 2
98
+ code 0
99
+ addition ; 16-bit addition routine: z = x + y
100
+ movf x, W
101
+ addwf y, W
102
+ movwf z
103
+ movf x + 1, W
104
+ btfsc STATUS, C
105
+ addlw 1
106
+ addwf y + 1, W
107
+ movwf z + 1
108
+ return
109
+ end
110
+
111
+ In `spec/spec_helper.rb`, we make a simulation class that points to the compiled COF file and defines the variables:
112
+
113
+ !!!ruby
114
+ require 'rpicsim/rspec'
115
+
116
+ class Addition < RPicSim::Sim
117
+ device_is "PIC10F322"
118
+ filename_is File.dirname(__FILE__) + "../firmware/dist/firmware.cof"
119
+ def_var :x, :u16
120
+ def_var :y, :u16
121
+ def_var :z, :u16
122
+ end
123
+
124
+ In `spec/addition_spec.rb`, we write a simple unit test that writes to `x` and `y`, runs the `addition` subroutine, and checks that the correct result is stored in `z`:
125
+
126
+ !!!ruby
127
+ require_relative 'spec_helper'
128
+
129
+ describe "addition routine" do
130
+ before do
131
+ start_sim Addition
132
+ end
133
+
134
+ it "can add 70 + 22"
135
+ x.value = 70
136
+ y.value = 22
137
+ run_subroutine :addition, cycle_limit: 100
138
+ expect(z.value).to eq 92
139
+ end
140
+ end
data/lib/rpicsim.rb ADDED
@@ -0,0 +1,15 @@
1
+ require_relative 'rpicsim/version'
2
+ require_relative 'rpicsim/sim'
3
+ require_relative 'rpicsim/call_stack_info'
4
+
5
+ # TODO: add a feature for Flash size reports
6
+ # TODO: add a feature for RAM usage reports
7
+
8
+ # TODO: add a feature for calculating the minimum and maximum iteration
9
+ # time from one point in the program to another
10
+ # (as a special case, this lets you calculate the iteration time of the
11
+ # main loop if both points are the same)
12
+
13
+ # TODO: add matchers: have_value(44), have_value.in(0..3), and maybe:
14
+ # have_value.between(0, 14) (like RSpec built-in matcher)
15
+ # have_value.within(3).of(56) (like RSpec built-in matcher)
@@ -0,0 +1,306 @@
1
+ require_relative 'search'
2
+ require 'set'
3
+
4
+ module RPicSim
5
+ # This class helps analyze programs to see whether it is possible for them
6
+ # to overflow the call stack, which is limited to a small number of levels on
7
+ # many PIC microcontrollers.
8
+ # It traverses the {Instruction} graph provided by a {ProgramFile}
9
+ # from a given root node and for each reachable instruction determines the
10
+ # maximum possible call stack depth when that instruction starts executing,
11
+ # relative to how deep the call stack depth was when the root instruction
12
+ # started.
13
+ #
14
+ # Here is an example of how you would use this to check the call stack depth
15
+ # in a program that has one interrupt vector at address 4 inside an RSpec test.
16
+ # Of course, you should adjust the numbers to suit your application:
17
+ #
18
+ # infos = CallStackInfo.hash_from_program_file(program_file, [0, 4])
19
+ # infos[0].max_depth.should <= 5 # main-line code should take at most 5 levels
20
+ # infos[4].max_depth.should <= 1 # ISR should take at most 1 stack level
21
+ #
22
+ # Additionally, it can generate reports of all different ways that the maximum
23
+ # call stack depth can be achieved, which can be helpful if you need to reduce
24
+ # your maximum stack depth.
25
+ class CallStackInfo
26
+ # For each of the given root instruction addresses, generates a {CallStackInfo}
27
+ # report. Returns the reports in a hash.
28
+ #
29
+ # @param program_file (ProgramFile)
30
+ # @param root_instruction_addresses Array(Integer) The flash
31
+ # addresses of the entry vectors for your program. On a midrange device, these
32
+ # are typically 0 for the main-line code and 4 for the interrupt.
33
+ # @return [Hash(address => CallStackInfo)]
34
+ def self.hash_from_program_file(program_file, root_instruction_addresses)
35
+ infos = {}
36
+ root_instruction_addresses.each do |addr|
37
+ infos[addr] = from_root_instruction(program_file.instruction(addr))
38
+ end
39
+ infos
40
+ end
41
+
42
+ # Generates a {CallStackInfo} from the given root instruction.
43
+ # This will tell you the maximum value the call stack could get to for a
44
+ # program that starts at the given instruction with an empty call stack
45
+ # and never gets interrupted.
46
+ def self.from_root_instruction(root)
47
+ new(root)
48
+ end
49
+
50
+ # The maximum call stack depth for all the reachable instructions.
51
+ # If your program starts executing at the root node and the call stack
52
+ # is empty, then (not accounting for interrupts) the call stack will
53
+ # never exceed this depth.
54
+ #
55
+ # A value of 0 means that no subroutine calls a possible; a value of
56
+ # 1 means that at most one subroutine call is possible at any given time,
57
+ # and so on.
58
+ #
59
+ # @return (Integer)
60
+ attr_reader :max_depth
61
+
62
+ # @return (Instruction) The root instruction that this report was generated from.
63
+ attr_reader :root
64
+
65
+ # Generates a {CallStackInfo} from the given root instruction.
66
+ def initialize(root)
67
+ @root = root
68
+ generate
69
+ @max_depth = @max_depths.values.max
70
+ end
71
+
72
+ private
73
+ def generate
74
+ @max_depths = { @root => 0 }
75
+ @back_links = Hash.new { [] }
76
+ instructions_to_process = [root]
77
+ while !instructions_to_process.empty?
78
+ instruction = instructions_to_process.pop
79
+ instruction.transitions.reverse_each do |transition|
80
+ ni = transition.next_instruction
81
+ prev_depth = @max_depths[ni]
82
+ new_depth = @max_depths[instruction] + transition.call_depth_change
83
+
84
+ if new_depth > 50
85
+ raise "Recursion probably detected. Maximum call depth of #{ni} is at least #{new_depth}."
86
+ end
87
+
88
+ if prev_depth.nil? || new_depth > prev_depth
89
+ #puts "%30s, MD=%d" % [ni, new_depth]
90
+ @max_depths[ni] = new_depth
91
+ instructions_to_process << ni
92
+ @back_links[ni] = []
93
+ end
94
+
95
+ if new_depth == @max_depths[ni]
96
+ #puts "Adding backlink #{ni} -> #{instruction}"
97
+ @back_links[ni] << instruction
98
+ end
99
+ end
100
+ end
101
+ end
102
+ public
103
+
104
+ # Returns all the {Instruction}s that have the worse case possible call stack depth.
105
+ # @return [Array(Instruction)]
106
+ def instructions_with_worst_case
107
+ @max_depths.select { |instr, depth| depth == @max_depth }.collect(&:first).sort
108
+ end
109
+
110
+ # Returns all the {Instruction}s that are reachable from the given root.
111
+ # @return [Array(Instruction)]
112
+ def reachable_instructions
113
+ @max_depths.keys
114
+ end
115
+
116
+ # Check the max-depth data hash for consistency.
117
+ def double_check!
118
+ reachable_instructions.each do |instr|
119
+ depth = @max_depths[instr]
120
+
121
+ instr.transitions.each do |transition|
122
+ next_instruction = transition.next_instruction
123
+ if @max_depths[next_instruction] < @max_depths[instr] + transition.call_depth_change
124
+ raise "Call stack info double check failed: %s has max_depth %d and leads (%d) to %s with max_depth %d." %
125
+ [instr, depth, transition.call_depth_change, next_instruction, @max_depths[next_instruction]]
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+
132
+ # Returns an array of {CodePath}s representing all possible ways that the call stack
133
+ # could reach the worst-case depth. This will often be a very large amount of data,
134
+ # even for a small project.
135
+ # @return [Array(CodePath)]
136
+ def worst_case_code_paths
137
+ instructions_with_worst_case.sort.flat_map do |instruction|
138
+ code_paths(instruction)
139
+ end
140
+ end
141
+
142
+ # Returns a filtered version of {#worst_case_code_paths}.
143
+ # Filters out any code paths that are just a superset of another code path.
144
+ # For each instruction that has a back trace leading to it, it just returns
145
+ # the code paths with the smallest number of interesting instructions.
146
+ # @return [Array(CodePath)]
147
+ def worst_case_code_paths_filtered
148
+ all_code_paths = worst_case_code_paths
149
+
150
+ #puts "all worst-case code paths: #{all_code_paths.size}"
151
+
152
+ # Filter out code path that are just a superset of another code path.
153
+ previously_seen_instruction_sequences = Set.new
154
+ code_paths = []
155
+ all_code_paths.sort_by(&:count).each do |code_path|
156
+ seen_before = (1..code_path.instructions.size).any? do |n|
157
+ subsequence = code_path.instructions[0, n]
158
+ previously_seen_instruction_sequences.include? subsequence
159
+ end
160
+ if !seen_before
161
+ previously_seen_instruction_sequences << code_path.instructions
162
+ code_paths << code_path
163
+ end
164
+ end
165
+
166
+ # For each instruction that has a code path leading to it, pick out
167
+ # the shortest code path (in terms of interesting instructions).
168
+ code_paths = code_paths.group_by { |cp| cp.instructions.last }.collect do |instr, code_paths|
169
+ code_paths.min_by { |cp| cp.interesting_instructions.count }
170
+ end
171
+
172
+ code_paths
173
+ end
174
+
175
+ # Returns a nice report string of all the {#worst_case_code_paths_filtered}.
176
+ # @return [String]
177
+ def worst_case_code_paths_filtered_report
178
+ s = ""
179
+ worst_case_code_paths_filtered.each do |code_path|
180
+ s << code_path.to_s + "\n"
181
+ s << "\n"
182
+ end
183
+ s
184
+ end
185
+
186
+ # @return [Array(CodePaths)] all the possible code paths that lead to the given instruction.
187
+ def code_paths(instruction)
188
+ code_paths = []
189
+ Search.depth_first_search_simple([[instruction]]) do |instrs|
190
+ prev_instrs = @back_links[instrs.first]
191
+
192
+ if prev_instrs.empty?
193
+ # This must be the root node.
194
+ if instrs.first != @root
195
+ raise "This instruction is not the root and has no back links: #{instrs}."
196
+ end
197
+
198
+ code_paths << CodePath.new(instrs)
199
+ [] # don't search anything from this node
200
+ else
201
+ # Investigate all possible code paths that could get to this instruction.
202
+ # However, exclude code paths that have the same instruction twice;
203
+ # otherwise we get stuck in an infinite loop.
204
+ (prev_instrs - instrs).collect do |instr|
205
+ [instr] + instrs
206
+ end
207
+ end
208
+ end
209
+ code_paths
210
+ end
211
+
212
+ def inspect
213
+ "#<#{self.class}:root=#{@root.inspect}>"
214
+ end
215
+
216
+ # This is a helper class for {CallStackInfo}. It wraps an array of {Instruction} objects
217
+ # representing an execution path from one part of the program (usually the entry vector or
218
+ # the ISR vector) to another part of the program.
219
+ # It has method for reducing this list of instructions by only showing the interesting ones.
220
+ class CodePath
221
+ include Enumerable
222
+
223
+ # An array of {Instruction}s that represents a possible path through the program.
224
+ # Each instruction in the list could possibly execute after the previous one.
225
+ attr_reader :instructions
226
+
227
+ # A new instance that wraps the given instructions.
228
+ # @param instructions Array(Instruction) The instructions to wrap.
229
+ def initialize(instructions)
230
+ @instructions = instructions.freeze
231
+ end
232
+
233
+ # Iterates over the wrapped instructions by calling <tt>each</tt> on the underlying array.
234
+ # Since this class also includes <tt>Enumerable</tt>, it means you can use any of the
235
+ # usual methods of Enumerable (e.g. <tt>select</tt>) on this class.
236
+ def each(&proc)
237
+ instructions.each(&proc)
238
+ end
239
+
240
+ # Returns the addresses of the underlying instructions.
241
+ def addresses
242
+ instructions.collect(&:address)
243
+ end
244
+
245
+ # Returns an array of the addresses of the interesting instructions.
246
+ def interesting_addresses
247
+ interesting_instructions.collect(&:address)
248
+ end
249
+
250
+ # Returns just the interesting instructions, as defined by {#interesting_instruction?}.
251
+ #
252
+ # @return [Array(Integer)]
253
+ def interesting_instructions
254
+ @instructions.select.each_with_index do |instruction, index|
255
+ next_instruction = @instructions[index + 1]
256
+ interesting_instruction?(instruction, next_instruction)
257
+ end
258
+ end
259
+
260
+ # Returns true if the given instruction is interesting. An instruction is
261
+ # interesting if you would need to see it in order to understand the path
262
+ # program has taken through the code and understand why the call stack
263
+ # could reach a certain depth.
264
+ #
265
+ # * The first and last instructions are interesting.
266
+ # * A branch that is taken is interesting.
267
+ # * A subroutine call is interesting.
268
+ def interesting_instruction?(instruction, next_instruction)
269
+ if instruction == @instructions.first || instruction == @instructions.last
270
+ return true
271
+ end
272
+
273
+ transition = instruction.transition_to(next_instruction)
274
+
275
+ if transition.call_depth_change >= 1
276
+ # This edge represents a function call so it is interesting.
277
+ return true
278
+ end
279
+
280
+ if transition.non_local?
281
+ # This edge represents a goto, so that is interesting
282
+ # because you need to know which gotos were taken to understand
283
+ # a code path. If seeing the skip is annoying we could
284
+ # suppress that by adding more information to the edge.
285
+ return true
286
+ end
287
+
288
+ # Everything else is not interesting.
289
+
290
+ # We are purposing deciding that skips are not interesting.
291
+ # because if you are trying to understand the full code path
292
+ # it is obvious whether any given skip was taken or not, as long
293
+ # as you know which calls and gotos were taken.
294
+
295
+ return false
296
+ end
297
+
298
+ # Returns a multi-line string representing this execution path.
299
+ def to_s
300
+ "CodePath:\n" +
301
+ interesting_instructions.join("\n") + "\n"
302
+ end
303
+ end
304
+
305
+ end
306
+ end