OXYGENHUB
◆ main openmsx / doc / internal / r800-call.txt
8.4 KiB · Plain Text · last changed chore(): initial commit 1 month ago source blame history raw
1
Some time ago (March 2015) Laurens Holst (aka Grauw) did some measurements
2
on the duration of various R800 instructions. For his results see
3
   http://map.grauw.nl/resources/z80instr.php
4
For the most part he was able to confirm the results we obtained earlier
5
(described in the "r800test.txt" document). Though for the CALL
6
instruction he 'sometimes' got a 1 cycle difference. Now, half a year
7
later, I finally got around to investigate this in more detail.
8
9
10
I used the following measuring method. Alex Wulms once created an MSX
11
cartridge with a 3.57MHz counter on it. Writing to IO port 0x20 resets the
12
counter and reading from IO ports 0x20-0x23 (atomically) reads it. So this
13
is much like the MSX-turboR E6-timer, except that it ticks 14x faster. The
14
R800 runs at 2 x 3.57MHz, so that means we can still only measure up-to 2
15
cycles accurate. To work around this I always repeat the to-be-measured
16
sequence twice. Here's an example program.
17
18
	org #c000
19
20
	di
21
	out (#20),a   ; reset timer
22
23
	call func     ; repeat to be measured
24
	call func     ;   sequence twice
25
26
	in a,(#20)    ; read timer
27
	ld l,a
28
	in a,(#20)
29
	ld h,a
30
	ret
31
32
func:	ret
33
34
Initially I run this program without the 2 'call func' instructions. After
35
the program returns, register HL contains the value '5'. This is the
36
time between reseting and reading the counter. This value should be
37
subtracted from all future measurements.
38
39
Note: usually these test programs give stable results. But once in a while
40
you get a too high value. This can be explained by the R800 refresh stuff
41
(see "r800-refresh.txt" for more details). By repeating the same test a
42
few times it's often possible to avoid this refresh stuff.
43
44
When re-inserting the two 'call func' instructions, I obtain the value
45
'17', subtracting 5 gives 12. So executing 2 'call + ret' sequences takes
46
2x12 cycles. So a single 'call + ret' sequence takes 12 cycles. Later in
47
this text I won't repeat this calculation, I'll directly give the length
48
of the (single) sequence.
49
50
So this confirms the result of my earlier measurements. See "r800test.txt"
51
for details. There I also show that these 12 cycles decompose into 7
52
cycles for the "call" instruction and 5 cycles for the "ret" instruction.
53
54
The interesting thing is that Laurens 'sometimes' measured 8 cycles for
55
the "call" instruction. Let's now make the following change in the program:
56
	...
57
func:	nop     ; <-- added this nop instruction
58
	ret
59
60
So the executed sequence is now "call + nop + ret". Naively we'd expect
61
this to take only 1 cycle more. Instead we measure 14 cycles (2 more).
62
63
I also tested this program:
64
	...
65
	call func
66
	nop
67
	call func
68
	nop
69
	...
70
func:	ret
71
72
The sequence is now "call + ret + nop". This does take 13 cycles (only 1
73
more). So it's really "call" immediately followed by "ret" that is
74
special.
75
76
I measured a lot of other sequences as well. I'll summarize them in this
77
table:
78
79
   sequence            measured  decomposed  remark
80
a) call, ret               12    7+5         NO penalty, call-ret is special
81
b) call, ret, nop          13    7+5+1       NO penalty
82
83
c) call, nop, ret          14    7+3+4       penalty on nop
84
d) call, nop, nop, ret     15    7+3+1+4     penalty on 1st nop
85
86
e) call, reti              15    7+8         penalty on reti, reti is NOT special
87
f) call, nop, reti         16    7+3+6       penalty on nop
88
89
g) call, pop hl            12    7+5         NO penalty, call-pop is special
90
h) call, nop, pop hl       14    7+3+4       penalty on nop
91
i) call, nop, nop, pop hl  15    7+3+1+4     penalty on 1st nop
92
93
j) call, pop ix            14    7+7         penalty on "pop ix", different from "pop hl"!
94
k) call, nop, pop ix       15    7+3+5       penalty on nop
95
l) call, nop, nop, pop ix  16    7+3+1+5     penalty on 1st nop
96
97
m) push hl, pop hl         11    6+5         no penalty,
98
n) push hl, nop, pop hl    12    6+2+4          push-pop is not special
99
o) push hl, ret            11    6+5         no penalty,
100
p) push hl, nop, ret       12    6+2+4          push-ret is not special
101
102
q) rst, ret                11    6+5         NO penalty, rst-ret is special
103
r) rst, nop, ret           13    6+3+4       penalty on nop
104
s) rst, nop, nop, ret      14    6+3+1+4     penalty on 1st nop
105
106
t) rst, reti               14    6+8         penalty on reti, reti is NOT special
107
u) rst, nop, reti          15    6+3+6       penalty on nop
108
v) rst, nop, nop, reti     16    6+3+1+6     penalty on 1st nop
109
110
w) rst, jp(hl)              9    6+3         penalty on jp(hl)
111
x) rst, jp(ix)             10    6+4         penalty on jp(ix)
112
113
114
Details:
115
116
a) For simplicity I've shown the decomposition 12 -> 7+5. In reality the
117
situation is more complex. The full sequence in the test-program is
118
  out ; call ; ret ; call ; ret ; in
119
120
When we take page-breaks into account, we get the following for the 4
121
middle instructions (I'm using the notation from the "r800test.txt"
122
document):
123
   fffWw FRr FffWw FRr + page-break
124
So the 1st "call" instruction takes 6 cycles, the 1st "ret" takes 5
125
cycles, 2nd "call" takes 7, 2nd "ret" takes 5 and the next "in"
126
instruction has a page-break-penalty on fetch. If we simplify this
127
(artificially attribute the page-break for "in" to the 1st "call") we can
128
say "call" takes 7 and "ret" takes 5 cycles.
129
130
Notice that there's an even number of cycles between the "out" and "in"
131
operation. So this inserts an IO-penalty on the IN instruction (to align
132
the R800 bus at 7MHZ to the external cartridge bus at 3.5MHz, see
133
"r800test.txt" for more details). In case of an empty sequence ("out"
134
directly followed by "in"), we have zero cycles which is also even. So our
135
calibration method (subtract 5 from the measured result) remains valid.
136
137
138
d) I'll give another example of the simplified decomposition. The full
139
sequence is:
140
  (out) ; call ; nop ; nop ; ret ; call ; nop ; nop ; ret ; (in)
141
The middle part (without out-in) decomposes to
142
  fffWw Fx f fRr FffWw Fx f fWw + page-break
143
    6   3  1  4    7   3  1  4    (+1)
144
Here 'x' is an extra "call-penalty", see below for more details. So if we
145
artificially move the page-break-penalty for "in" to the front we get
146
7+3+1+4 for call+nop+nop+ret. This same simplification is made for all
147
decompositions in the table.
148
149
150
c) Compared to 'a' you'd expect this sequence to take only 1 extra cycle
151
(just like sequence b). Instead there's yet 1 more extra cycle. In
152
general, when looking at more sequences and/or replacing 'nop' with other
153
instructions with known duration (not shown in the table), we see that we
154
always get an extra cycle whenever a call instruction is not _immediately_
155
followed by a "ret" or "pop" instruction. We'll call this a "call-penalty"
156
cycle.
157
158
We could attribute this call-penalty cycle to the call instruction itself,
159
thus say that the call instruction takes 7 or 8 cycles, depending on what
160
instruction follows. For most practical purposes this explanation is good
161
enough. Though if you look in detail, the fetch of the next instruction
162
has to start 7 (not 8) cycles after the start of the call instruction
163
(otherwise we cannot know whether there should be a penalty or not). So
164
this call-penalty really happens during the _next_ instruction.
165
166
   It's likely that this call-penalty stuff can be explained in a simpler
167
   way if you look at the full-pipelined implementation of the R800.
168
   _Maybe_ a call somehow/sometimes introduces a late pipeline stall.
169
   Though because no details are known about the R800 pipeline, for now,
170
   I'll stick to this weird sequential explanation that a "call"
171
   instruction somehow induces a stall in the next instruction.
172
173
174
e) In sequence 'a' and 'b' we saw that "call" immediately followed by
175
"ret" has no call-penalty. This is _not_ the case for "reti" or "retn".
176
177
178
g) h) i) A "call" immediately followed by "pop" also has no penalty.
179
180
j) k) l) Though "pop" has to be a single-byte instruction ("pop af", "pop
181
bc", "pop de" or "pop hl"). "pop ix" or "pop iy" do have the call-penalty.
182
This is similar to how a multi-byte return instruction (reti) also gets
183
the penalty.
184
185
186
m) n) o) p) From a stack-usage point of view a "call" immediately followed
187
by a "ret" or "pop" is similar to a "push" immediately followed by "ret"
188
or "pop". Though the tests show there's no penalty for push-nop-pop
189
compared to push-pop or for push-nop-ret compared to push-ret.
190
191
q) r) s) t) u) v) The "rst" instruction behaves similar to "call", so
192
there is a penalty on the next instruction except if that next instruction
193
is a (single-byte) "ret" or "pop". The only difference is that the "rst"
194
instruction itself takes 1 (only 1!) cycles less than a "call"
195
instruction.
196
197
198
199
TODO implement this stuff in openMSX.