rpicsim 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +36 -0
- data/Gemfile +10 -0
- data/Introduction.md +64 -0
- data/LICENSE.txt +24 -0
- data/README.md +66 -0
- data/docs/ChangeLog.md +10 -0
- data/docs/Contributing.md +30 -0
- data/docs/Debugging.md +96 -0
- data/docs/DefiningSimulationClass.md +84 -0
- data/docs/DesignDecisions.md +48 -0
- data/docs/HowMPLABXIsFound.md +14 -0
- data/docs/IntegrationTesting.md +15 -0
- data/docs/IntroductionToRSpec.md +203 -0
- data/docs/IntroductionToRuby.md +90 -0
- data/docs/KnownIssues.md +204 -0
- data/docs/Labels.md +39 -0
- data/docs/MakingTestsRunFaster.md +29 -0
- data/docs/Manual.md +38 -0
- data/docs/PersistentExpectations.md +100 -0
- data/docs/Pins.md +143 -0
- data/docs/PreventingCallStackOverflow.md +94 -0
- data/docs/QuickStartGuide.md +85 -0
- data/docs/RSpecIntegration.md +119 -0
- data/docs/RamWatcher.md +46 -0
- data/docs/Running.md +129 -0
- data/docs/SFRs.md +71 -0
- data/docs/Stubbing.md +161 -0
- data/docs/SupportedCompilers.md +5 -0
- data/docs/SupportedDevices.md +14 -0
- data/docs/SupportedMPLABXVersions.md +12 -0
- data/docs/SupportedOperatingSystems.md +3 -0
- data/docs/UnitTesting.md +9 -0
- data/docs/Variables.md +140 -0
- data/lib/rpicsim.rb +15 -0
- data/lib/rpicsim/call_stack_info.rb +306 -0
- data/lib/rpicsim/flaws.rb +76 -0
- data/lib/rpicsim/instruction.rb +178 -0
- data/lib/rpicsim/label.rb +28 -0
- data/lib/rpicsim/memory.rb +29 -0
- data/lib/rpicsim/memory_watcher.rb +98 -0
- data/lib/rpicsim/mplab.rb +138 -0
- data/lib/rpicsim/mplab/mplab_assembly.rb +102 -0
- data/lib/rpicsim/mplab/mplab_device_info.rb +40 -0
- data/lib/rpicsim/mplab/mplab_disassembler.rb +23 -0
- data/lib/rpicsim/mplab/mplab_instruction.rb +32 -0
- data/lib/rpicsim/mplab/mplab_memory.rb +33 -0
- data/lib/rpicsim/mplab/mplab_nmmr_info.rb +21 -0
- data/lib/rpicsim/mplab/mplab_observer.rb +12 -0
- data/lib/rpicsim/mplab/mplab_pin.rb +61 -0
- data/lib/rpicsim/mplab/mplab_processor.rb +30 -0
- data/lib/rpicsim/mplab/mplab_program_file.rb +35 -0
- data/lib/rpicsim/mplab/mplab_register.rb +25 -0
- data/lib/rpicsim/mplab/mplab_sfr_info.rb +21 -0
- data/lib/rpicsim/mplab/mplab_simulator.rb +102 -0
- data/lib/rpicsim/pin.rb +61 -0
- data/lib/rpicsim/program_counter.rb +19 -0
- data/lib/rpicsim/program_file.rb +160 -0
- data/lib/rpicsim/register.rb +78 -0
- data/lib/rpicsim/rspec.rb +11 -0
- data/lib/rpicsim/rspec/be_predicate.rb +12 -0
- data/lib/rpicsim/rspec/helpers.rb +48 -0
- data/lib/rpicsim/rspec/persistent_expectations.rb +53 -0
- data/lib/rpicsim/rspec/sim_diagnostics.rb +51 -0
- data/lib/rpicsim/search.rb +20 -0
- data/lib/rpicsim/sim.rb +702 -0
- data/lib/rpicsim/stack_trace.rb +32 -0
- data/lib/rpicsim/variable.rb +236 -0
- data/lib/rpicsim/version.rb +3 -0
- metadata +114 -0
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.
|
data/docs/UnitTesting.md
ADDED
@@ -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
|