__builtin_avr_delay_cycles 实现说明
__builtin_avr_delay_cycles implementation explanation
让我们考虑一下为 atmega32 编译的这个简单的 C 代码:
_delay_ms(1000);
它被翻译成这个程序集:
00000039 SER R18 Set Register
0000003A LDI R24,0x69 Load immediate
0000003B LDI R25,0x18 Load immediate
0000003C SUBI R18,0x01 Subtract immediate
0000003D SBCI R24,0x00 Subtract immediate with carry
0000003E SBCI R25,0x00 Subtract immediate with carry
0000003F BRNE PC-0x03 Branch if not equal
00000040 RJMP PC+0x0001 Relative jump
00000041 NOP No operation
当这一行:
_delay_ms(500);
编译成这样:
00000039 SER R18 Set Register
0000003A LDI R24,0x34 Load immediate
0000003B LDI R25,0x0C Load immediate
0000003C SUBI R18,0x01 Subtract immediate
0000003D SBCI R24,0x00 Subtract immediate with carry
0000003E SBCI R25,0x00 Subtract immediate with carry
0000003F BRNE PC-0x03 Branch if not equal
00000040 RJMP PC+0x0001 Relative jump
00000041 NOP No operation
有人可以解释生成的程序集背后的逻辑吗?内置函数的开发人员如何确保 __builtin_avr_delay_cycles
函数的确切周期延迟?
编辑:显然没有在文件顶部提到 #define F_CPU 8000000UL
!
可在此处访问 AVR 指令集手册:
http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf
让我们看看您的代码 _delay_ms(1000);
。
开头的ser、ldi、ldi指令需要3个周期,设置一个24位的计数器为0x1869FF
然后我们有一个循环。此循环的每次迭代都会将 24 位计数器减 1,一旦计数器达到零,循环就会终止。所以循环会有0x1869FF次迭代。
循环的大多数迭代需要 5 个周期,因为 brne 在分支时需要两个周期。在循环的最后一次迭代中,brne 没有分支,因此最后一次迭代只需要 4 个循环。
循环后的两条指令总共需要3个周期。
将所有内容相加,我们看到循环总数为:
3 + 0x1869FF*5 - 1 + 3 = 8000000
由于您的 CPU 速度为 8 MHz,因此延迟为 1 秒。
编译器实现者的诀窍是选择要执行多少次循环迭代、循环后要执行多少次 rjmp 以及之后要执行多少次 nop。由于在程序 space 方面向循环添加迭代是免费的,因此您希望拥有尽可能多的循环迭代。由于一个 rjmp 比两个 nop 占用的 space 更少,因此您希望在循环后拥有尽可能多的 rjmp。然后您可能需要一个 nop 才能使循环计数正确。
为了缩短延迟,编译器可能使用 8 位或 16 位计数器。对于非常短的延迟,它可能只使用 rjmp 和 nop。对于很长的延迟,它可能使用超过 24 位的计数器。
在某些情况下,可能可以通过在循环内添加几个 nop 来节省程序 space,因为它可能会产生更小的循环计数器,或者可能让您在末尾删除多条指令。所以一个真正聪明的实施会考虑到这一点。
让我们考虑一下为 atmega32 编译的这个简单的 C 代码:
_delay_ms(1000);
它被翻译成这个程序集:
00000039 SER R18 Set Register
0000003A LDI R24,0x69 Load immediate
0000003B LDI R25,0x18 Load immediate
0000003C SUBI R18,0x01 Subtract immediate
0000003D SBCI R24,0x00 Subtract immediate with carry
0000003E SBCI R25,0x00 Subtract immediate with carry
0000003F BRNE PC-0x03 Branch if not equal
00000040 RJMP PC+0x0001 Relative jump
00000041 NOP No operation
当这一行:
_delay_ms(500);
编译成这样:
00000039 SER R18 Set Register
0000003A LDI R24,0x34 Load immediate
0000003B LDI R25,0x0C Load immediate
0000003C SUBI R18,0x01 Subtract immediate
0000003D SBCI R24,0x00 Subtract immediate with carry
0000003E SBCI R25,0x00 Subtract immediate with carry
0000003F BRNE PC-0x03 Branch if not equal
00000040 RJMP PC+0x0001 Relative jump
00000041 NOP No operation
有人可以解释生成的程序集背后的逻辑吗?内置函数的开发人员如何确保 __builtin_avr_delay_cycles
函数的确切周期延迟?
编辑:显然没有在文件顶部提到 #define F_CPU 8000000UL
!
可在此处访问 AVR 指令集手册:
http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf
让我们看看您的代码 _delay_ms(1000);
。
开头的ser、ldi、ldi指令需要3个周期,设置一个24位的计数器为0x1869FF
然后我们有一个循环。此循环的每次迭代都会将 24 位计数器减 1,一旦计数器达到零,循环就会终止。所以循环会有0x1869FF次迭代。
循环的大多数迭代需要 5 个周期,因为 brne 在分支时需要两个周期。在循环的最后一次迭代中,brne 没有分支,因此最后一次迭代只需要 4 个循环。
循环后的两条指令总共需要3个周期。
将所有内容相加,我们看到循环总数为:
3 + 0x1869FF*5 - 1 + 3 = 8000000
由于您的 CPU 速度为 8 MHz,因此延迟为 1 秒。
编译器实现者的诀窍是选择要执行多少次循环迭代、循环后要执行多少次 rjmp 以及之后要执行多少次 nop。由于在程序 space 方面向循环添加迭代是免费的,因此您希望拥有尽可能多的循环迭代。由于一个 rjmp 比两个 nop 占用的 space 更少,因此您希望在循环后拥有尽可能多的 rjmp。然后您可能需要一个 nop 才能使循环计数正确。
为了缩短延迟,编译器可能使用 8 位或 16 位计数器。对于非常短的延迟,它可能只使用 rjmp 和 nop。对于很长的延迟,它可能使用超过 24 位的计数器。
在某些情况下,可能可以通过在循环内添加几个 nop 来节省程序 space,因为它可能会产生更小的循环计数器,或者可能让您在末尾删除多条指令。所以一个真正聪明的实施会考虑到这一点。