関数millis
は、100マイクロ秒以下のスパンで実行されます。1ミリ秒の通話にかかった時間を測定する信頼できる方法はありますか?
頭に浮かぶアプローチの1つはを使用することですmicros
が、への呼び出しにmicros
は、micros
それ自体の関数呼び出しにかかる時間millis
も含まれるため、マイクロの所要時間によっては、測定がオフになる場合があります。
私が取り組んでいるアプリケーションとして、これを見つける必要がありますmillis
。
miilis
かかる時間。
関数millis
は、100マイクロ秒以下のスパンで実行されます。1ミリ秒の通話にかかった時間を測定する信頼できる方法はありますか?
頭に浮かぶアプローチの1つはを使用することですmicros
が、への呼び出しにmicros
は、micros
それ自体の関数呼び出しにかかる時間millis
も含まれるため、マイクロの所要時間によっては、測定がオフになる場合があります。
私が取り組んでいるアプリケーションとして、これを見つける必要がありますmillis
。
miilis
かかる時間。
回答:
どれだけ時間がかかるかを正確に知りたい場合は、解決策は1つだけです。逆アセンブリを見てください。
最小限のコードから始めます:
void setup(){};
volatile uint16_t x;
void loop()
{
x = millis();
}
このコードはコンパイルされavr-objdump -S
てから、ドキュメント化された逆アセンブリを生成します。興味深い抜粋は次のとおりです。
void loop()
生成するもの:
000000a8 <loop>:
a8: 0e 94 a7 00 call 0x14e ; 0x14e <millis>
ac: 60 93 00 01 sts 0x0100, r22
b0: 70 93 01 01 sts 0x0101, r23
b4: 80 93 02 01 sts 0x0102, r24
b8: 90 93 03 01 sts 0x0103, r25
bc: 08 95 ret
これは関数呼び出し(call
)、4つのコピー(uint32_t
戻り値の各バイトをコピーしますmillis()
(arduinoのドキュメントではこれを呼び出しますlong
が、変数サイズを明示的に指定しないのは正しくありません))、最後に関数が戻ります。
call
4クロックサイクルをsts
必要とし、それぞれ2クロックサイクルを必要とします。そのため、関数呼び出しのオーバーヘッドのために最低12クロックサイクルが必要です。
次に、次の<millis>
場所にある関数の逆アセンブリを見てみましょう0x14e
。
unsigned long millis()
{
unsigned long m;
uint8_t oldSREG = SREG;
14e: 8f b7 in r24, 0x3f ; 63
// disable interrupts while we read timer0_millis or we might get an
// inconsistent value (e.g. in the middle of a write to timer0_millis)
cli();
150: f8 94 cli
m = timer0_millis;
152: 20 91 08 01 lds r18, 0x0108
156: 30 91 09 01 lds r19, 0x0109
15a: 40 91 0a 01 lds r20, 0x010A
15e: 50 91 0b 01 lds r21, 0x010B
SREG = oldSREG;
162: 8f bf out 0x3f, r24 ; 63
return m;
}
164: b9 01 movw r22, r18
166: ca 01 movw r24, r20
168: 08 95 ret
ご覧のとおり、millis()
関数は非常に簡単です。
in
割り込みレジスタの設定を保存します(1サイクル)cli
割り込みをオフにします(1サイクル)lds
ミリカウンタの現在値の4バイトの1つを一時レジスタにコピーします(2クロックサイクル)lds
バイト2(2クロックサイクル)lds
バイト3(2クロックサイクル)lds
バイト4(2クロックサイクル)out
割り込み設定の復元(1クロックサイクル)movw
レジスタをシャッフルします(1クロックサイクル)movw
そして再び(1クロックサイクル)ret
サブルーチンから戻る(4サイクル)したがって、それらをすべて合計すると、millis()
関数自体に合計17クロックサイクルがあり、さらに12の呼び出しオーバーヘッドがあり、合計29クロックサイクルになります。
16 Mhzクロックレート(ほとんどのarduinos)を想定すると、各クロックサイクルは1 / 16e6
秒、つまり0.0000000625秒(62.5ナノ秒)です。62.5 ns * 29 = 1.812マイクロ秒。
したがって、ほとんどの Arduinoでの1回のmillis()
呼び出しの合計実行時間は1.812マイクロ秒になります。
補足として、ここには最適化のためのスペースがあります!unsigned long millis(){}
関数定義をに更新inline unsigned long millis(){}
すると、呼び出しのオーバーヘッドがなくなります(コードサイズがわずかに大きくなります)。さらに、コンパイラーが2つの不必要な移動を行っているように見えます(2つのmovw
呼び出しですが、私はそれほど詳しく調べていません)。
実際、関数呼び出しのオーバーヘッドが5命令であり、関数の実際の内容がmillis()
6命令しかないことを考えると、millis()
関数はinline
デフォルトで実際にあるはずですが、Arduinoコードベースはかなり不十分に最適化されています。
興味のある人のための完全な分解は次のとおりです。
sketch_feb13a.cpp.elf: file format elf32-avr
Disassembly of section .text:
00000000 <__vectors>:
SREG = oldSREG;
return m;
}
unsigned long micros() {
0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end>
4: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
8: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
c: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
10: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
14: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
18: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
1c: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
20: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
24: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
28: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
2c: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
30: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
34: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
38: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
3c: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
40: 0c 94 5f 00 jmp 0xbe ; 0xbe <__vector_16>
44: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
48: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
4c: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
50: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
54: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
58: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
5c: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
60: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
64: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt>
00000068 <__ctors_end>:
68: 11 24 eor r1, r1
6a: 1f be out 0x3f, r1 ; 63
6c: cf ef ldi r28, 0xFF ; 255
6e: d8 e0 ldi r29, 0x08 ; 8
70: de bf out 0x3e, r29 ; 62
72: cd bf out 0x3d, r28 ; 61
00000074 <__do_copy_data>:
74: 11 e0 ldi r17, 0x01 ; 1
76: a0 e0 ldi r26, 0x00 ; 0
78: b1 e0 ldi r27, 0x01 ; 1
7a: e2 e0 ldi r30, 0x02 ; 2
7c: f2 e0 ldi r31, 0x02 ; 2
7e: 02 c0 rjmp .+4 ; 0x84 <.do_copy_data_start>
00000080 <.do_copy_data_loop>:
80: 05 90 lpm r0, Z+
82: 0d 92 st X+, r0
00000084 <.do_copy_data_start>:
84: a0 30 cpi r26, 0x00 ; 0
86: b1 07 cpc r27, r17
88: d9 f7 brne .-10 ; 0x80 <.do_copy_data_loop>
0000008a <__do_clear_bss>:
8a: 11 e0 ldi r17, 0x01 ; 1
8c: a0 e0 ldi r26, 0x00 ; 0
8e: b1 e0 ldi r27, 0x01 ; 1
90: 01 c0 rjmp .+2 ; 0x94 <.do_clear_bss_start>
00000092 <.do_clear_bss_loop>:
92: 1d 92 st X+, r1
00000094 <.do_clear_bss_start>:
94: ad 30 cpi r26, 0x0D ; 13
96: b1 07 cpc r27, r17
98: e1 f7 brne .-8 ; 0x92 <.do_clear_bss_loop>
9a: 0e 94 f0 00 call 0x1e0 ; 0x1e0 <main>
9e: 0c 94 ff 00 jmp 0x1fe ; 0x1fe <_exit>
000000a2 <__bad_interrupt>:
a2: 0c 94 00 00 jmp 0 ; 0x0 <__vectors>
000000a6 <setup>:
a6: 08 95 ret
000000a8 <loop>:
a8: 0e 94 a7 00 call 0x14e ; 0x14e <millis>
ac: 60 93 00 01 sts 0x0100, r22
b0: 70 93 01 01 sts 0x0101, r23
b4: 80 93 02 01 sts 0x0102, r24
b8: 90 93 03 01 sts 0x0103, r25
bc: 08 95 ret
000000be <__vector_16>:
#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
be: 1f 92 push r1
c0: 0f 92 push r0
c2: 0f b6 in r0, 0x3f ; 63
c4: 0f 92 push r0
c6: 11 24 eor r1, r1
c8: 2f 93 push r18
ca: 3f 93 push r19
cc: 8f 93 push r24
ce: 9f 93 push r25
d0: af 93 push r26
d2: bf 93 push r27
// copy these to local variables so they can be stored in registers
// (volatile variables must be read from memory on every access)
unsigned long m = timer0_millis;
d4: 80 91 08 01 lds r24, 0x0108
d8: 90 91 09 01 lds r25, 0x0109
dc: a0 91 0a 01 lds r26, 0x010A
e0: b0 91 0b 01 lds r27, 0x010B
unsigned char f = timer0_fract;
e4: 30 91 0c 01 lds r19, 0x010C
m += MILLIS_INC;
e8: 01 96 adiw r24, 0x01 ; 1
ea: a1 1d adc r26, r1
ec: b1 1d adc r27, r1
f += FRACT_INC;
ee: 23 2f mov r18, r19
f0: 2d 5f subi r18, 0xFD ; 253
if (f >= FRACT_MAX) {
f2: 2d 37 cpi r18, 0x7D ; 125
f4: 20 f0 brcs .+8 ; 0xfe <__vector_16+0x40>
f -= FRACT_MAX;
f6: 2d 57 subi r18, 0x7D ; 125
m += 1;
f8: 01 96 adiw r24, 0x01 ; 1
fa: a1 1d adc r26, r1
fc: b1 1d adc r27, r1
}
timer0_fract = f;
fe: 20 93 0c 01 sts 0x010C, r18
timer0_millis = m;
102: 80 93 08 01 sts 0x0108, r24
106: 90 93 09 01 sts 0x0109, r25
10a: a0 93 0a 01 sts 0x010A, r26
10e: b0 93 0b 01 sts 0x010B, r27
timer0_overflow_count++;
112: 80 91 04 01 lds r24, 0x0104
116: 90 91 05 01 lds r25, 0x0105
11a: a0 91 06 01 lds r26, 0x0106
11e: b0 91 07 01 lds r27, 0x0107
122: 01 96 adiw r24, 0x01 ; 1
124: a1 1d adc r26, r1
126: b1 1d adc r27, r1
128: 80 93 04 01 sts 0x0104, r24
12c: 90 93 05 01 sts 0x0105, r25
130: a0 93 06 01 sts 0x0106, r26
134: b0 93 07 01 sts 0x0107, r27
}
138: bf 91 pop r27
13a: af 91 pop r26
13c: 9f 91 pop r25
13e: 8f 91 pop r24
140: 3f 91 pop r19
142: 2f 91 pop r18
144: 0f 90 pop r0
146: 0f be out 0x3f, r0 ; 63
148: 0f 90 pop r0
14a: 1f 90 pop r1
14c: 18 95 reti
0000014e <millis>:
unsigned long millis()
{
unsigned long m;
uint8_t oldSREG = SREG;
14e: 8f b7 in r24, 0x3f ; 63
// disable interrupts while we read timer0_millis or we might get an
// inconsistent value (e.g. in the middle of a write to timer0_millis)
cli();
150: f8 94 cli
m = timer0_millis;
152: 20 91 08 01 lds r18, 0x0108
156: 30 91 09 01 lds r19, 0x0109
15a: 40 91 0a 01 lds r20, 0x010A
15e: 50 91 0b 01 lds r21, 0x010B
SREG = oldSREG;
162: 8f bf out 0x3f, r24 ; 63
return m;
}
164: b9 01 movw r22, r18
166: ca 01 movw r24, r20
168: 08 95 ret
0000016a <init>:
void init()
{
// this needs to be called before setup() or some functions won't
// work there
sei();
16a: 78 94 sei
// on the ATmega168, timer 0 is also used for fast hardware pwm
// (using phase-correct PWM would mean that timer 0 overflowed half as often
// resulting in different millis() behavior on the ATmega8 and ATmega168)
#if defined(TCCR0A) && defined(WGM01)
sbi(TCCR0A, WGM01);
16c: 84 b5 in r24, 0x24 ; 36
16e: 82 60 ori r24, 0x02 ; 2
170: 84 bd out 0x24, r24 ; 36
sbi(TCCR0A, WGM00);
172: 84 b5 in r24, 0x24 ; 36
174: 81 60 ori r24, 0x01 ; 1
176: 84 bd out 0x24, r24 ; 36
// this combination is for the standard atmega8
sbi(TCCR0, CS01);
sbi(TCCR0, CS00);
#elif defined(TCCR0B) && defined(CS01) && defined(CS00)
// this combination is for the standard 168/328/1280/2560
sbi(TCCR0B, CS01);
178: 85 b5 in r24, 0x25 ; 37
17a: 82 60 ori r24, 0x02 ; 2
17c: 85 bd out 0x25, r24 ; 37
sbi(TCCR0B, CS00);
17e: 85 b5 in r24, 0x25 ; 37
180: 81 60 ori r24, 0x01 ; 1
182: 85 bd out 0x25, r24 ; 37
// enable timer 0 overflow interrupt
#if defined(TIMSK) && defined(TOIE0)
sbi(TIMSK, TOIE0);
#elif defined(TIMSK0) && defined(TOIE0)
sbi(TIMSK0, TOIE0);
184: ee e6 ldi r30, 0x6E ; 110
186: f0 e0 ldi r31, 0x00 ; 0
188: 80 81 ld r24, Z
18a: 81 60 ori r24, 0x01 ; 1
18c: 80 83 st Z, r24
// this is better for motors as it ensures an even waveform
// note, however, that fast pwm mode can achieve a frequency of up
// 8 MHz (with a 16 MHz clock) at 50% duty cycle
#if defined(TCCR1B) && defined(CS11) && defined(CS10)
TCCR1B = 0;
18e: e1 e8 ldi r30, 0x81 ; 129
190: f0 e0 ldi r31, 0x00 ; 0
192: 10 82 st Z, r1
// set timer 1 prescale factor to 64
sbi(TCCR1B, CS11);
194: 80 81 ld r24, Z
196: 82 60 ori r24, 0x02 ; 2
198: 80 83 st Z, r24
#if F_CPU >= 8000000L
sbi(TCCR1B, CS10);
19a: 80 81 ld r24, Z
19c: 81 60 ori r24, 0x01 ; 1
19e: 80 83 st Z, r24
sbi(TCCR1, CS10);
#endif
#endif
// put timer 1 in 8-bit phase correct pwm mode
#if defined(TCCR1A) && defined(WGM10)
sbi(TCCR1A, WGM10);
1a0: e0 e8 ldi r30, 0x80 ; 128
1a2: f0 e0 ldi r31, 0x00 ; 0
1a4: 80 81 ld r24, Z
1a6: 81 60 ori r24, 0x01 ; 1
1a8: 80 83 st Z, r24
// set timer 2 prescale factor to 64
#if defined(TCCR2) && defined(CS22)
sbi(TCCR2, CS22);
#elif defined(TCCR2B) && defined(CS22)
sbi(TCCR2B, CS22);
1aa: e1 eb ldi r30, 0xB1 ; 177
1ac: f0 e0 ldi r31, 0x00 ; 0
1ae: 80 81 ld r24, Z
1b0: 84 60 ori r24, 0x04 ; 4
1b2: 80 83 st Z, r24
// configure timer 2 for phase correct pwm (8-bit)
#if defined(TCCR2) && defined(WGM20)
sbi(TCCR2, WGM20);
#elif defined(TCCR2A) && defined(WGM20)
sbi(TCCR2A, WGM20);
1b4: e0 eb ldi r30, 0xB0 ; 176
1b6: f0 e0 ldi r31, 0x00 ; 0
1b8: 80 81 ld r24, Z
1ba: 81 60 ori r24, 0x01 ; 1
1bc: 80 83 st Z, r24
#if defined(ADCSRA)
// set a2d prescale factor to 128
// 16 MHz / 128 = 125 KHz, inside the desired 50-200 KHz range.
// XXX: this will not work properly for other clock speeds, and
// this code should use F_CPU to determine the prescale factor.
sbi(ADCSRA, ADPS2);
1be: ea e7 ldi r30, 0x7A ; 122
1c0: f0 e0 ldi r31, 0x00 ; 0
1c2: 80 81 ld r24, Z
1c4: 84 60 ori r24, 0x04 ; 4
1c6: 80 83 st Z, r24
sbi(ADCSRA, ADPS1);
1c8: 80 81 ld r24, Z
1ca: 82 60 ori r24, 0x02 ; 2
1cc: 80 83 st Z, r24
sbi(ADCSRA, ADPS0);
1ce: 80 81 ld r24, Z
1d0: 81 60 ori r24, 0x01 ; 1
1d2: 80 83 st Z, r24
// enable a2d conversions
sbi(ADCSRA, ADEN);
1d4: 80 81 ld r24, Z
1d6: 80 68 ori r24, 0x80 ; 128
1d8: 80 83 st Z, r24
// here so they can be used as normal digital i/o; they will be
// reconnected in Serial.begin()
#if defined(UCSRB)
UCSRB = 0;
#elif defined(UCSR0B)
UCSR0B = 0;
1da: 10 92 c1 00 sts 0x00C1, r1
#endif
}
1de: 08 95 ret
000001e0 <main>:
#include <Arduino.h>
int main(void)
1e0: cf 93 push r28
1e2: df 93 push r29
{
init();
1e4: 0e 94 b5 00 call 0x16a ; 0x16a <init>
#if defined(USBCON)
USBDevice.attach();
#endif
setup();
1e8: 0e 94 53 00 call 0xa6 ; 0xa6 <setup>
for (;;) {
loop();
if (serialEventRun) serialEventRun();
1ec: c0 e0 ldi r28, 0x00 ; 0
1ee: d0 e0 ldi r29, 0x00 ; 0
#endif
setup();
for (;;) {
loop();
1f0: 0e 94 54 00 call 0xa8 ; 0xa8 <loop>
if (serialEventRun) serialEventRun();
1f4: 20 97 sbiw r28, 0x00 ; 0
1f6: e1 f3 breq .-8 ; 0x1f0 <main+0x10>
1f8: 0e 94 00 00 call 0 ; 0x0 <__vectors>
1fc: f9 cf rjmp .-14 ; 0x1f0 <main+0x10>
000001fe <_exit>:
1fe: f8 94 cli
00000200 <__stop_program>:
200: ff cf rjmp .-2 ; 0x200 <__stop_program>
sts
は呼び出しのオーバーヘッドとしてカウントされるべきではありません。これは、結果を揮発性変数に格納するコストであり、通常は行いません。2)私のシステム(Arduino 1.0.5、gcc 4.8.2)では、movw
s がありません。呼び出しのコストmillis()
は次のとおりです。4サイクルの呼び出しオーバーヘッド+ millis()
それ自体で15サイクル=合計19サイクル(≈1.188 µs @ 16 MHz)。
x
ですuint16_t
。それが原因である場合、最大で2コピーである必要があります。とにかく、問題は、結果を無視して呼び出されたときではなく、使用されたときにどれくらい時間がmillis()
かかるかです。実際の使用には結果で何かを行う必要があるため、結果をを介して強制的に保存しました。通常、呼び出しの戻り値に設定された変数を後で使用しても同じ効果が得られますが、その余分な呼び出しが応答のスペースを占有することは望みませんでした。volatile
uint16_t
ソース内のこれはアセンブリと一致しません(4バイトがRAMに保存されます)。おそらく2つの異なるバージョンのソースと逆アセンブリを投稿したでしょう。
ループを作成するのではなく、コピーして貼り付けることで、1000ミリ秒のスケッチを書きます。それを測定し、実際の予想時間と比較します。IDE(および特にそのコンパイラ)のバージョンによって結果が異なる場合があることに注意してください。
もう1つのオプションは、ミリ秒呼び出しの前後にIOピンを切り替え、非常に小さい値とやや大きい値の時間を測定することです。測定されたタイミングを比較し、オーバーヘッドを計算します。
最も正確な方法は、生成されたコードである逆アセンブリリストを確認することです。しかし、それは弱い人のためではありません。各命令サイクルにかかる時間は、データシートを注意深く調べる必要があります。
millis()
通話にかかった時間をどのように測定しますか?
delay
あなたは正しい。しかし、考え方は同じままで、多数のコールの時間を計り、それらを平均化できます。ただし、グローバルに割り込みをオフにするのはあまり良い考えではないかもしれません; o)
私は2度目の繰り返しミリスを呼び出してから、実際と予想を比較しました。
オーバーヘッドは最小限に抑えられますが、millis()を呼び出す回数が増えるほど重要性は低下します。
見ると
C:\Program Files (x86)\Arduino\Arduino ERW 1.0.5\hardware\arduino\cores\arduino\wiring.c
millis()はわずか4命令(cli is simply # define cli() \__asm__ \__volatile__ ("cli" ::))
で戻り値が非常に小さいことがわかります。
条件としてvolatileを持つFORループを使用して、約1,000万回呼び出します。volatileキーワードは、コンパイラーがループ自体の最適化を試みるのを防ぎます。
以下が構文的に完璧であることを保証しません。
int temp1,temp2;
temp1=millis();
for (volatile unsigned int j=0;j<1000000;++j){
temp2=millis();}
Serial.print("Execution time = ");
Serial.print((temp2-temp1,DEC);
Serial.print("ms");
私の推測では、ミリ秒の呼び出しごとに約900msまたは約56usかかります。(私はaruduinoの便利なATMを持っていません。
int temp1,temp2;
しvolatile int temp1,temp2;
、潜在的にそれらを離れて最適化から、コンパイラを防ぐために。