让我们考虑一下为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
!
答案 0 :(得分:6)
可在此处访问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次数。由于就程序空间而言,将迭代添加到循环是免费的,因此您希望有尽可能多的循环迭代。由于一个rjmp占用的空间少于两次nop,因此您希望在循环后拥有尽可能多的rjmp。然后您可能需要先点一下以使周期计数正确。
对于较短的延迟,编译器可能使用8位或16位计数器。对于非常短的延迟,它可能仅使用rjmp和nop。对于很长的延迟,它可能使用超过24位的计数器。
在某些情况下,可以通过在循环内部添加一些nop来节省程序空间,因为这可能使循环计数器更小,或者可能使您最后删除多条指令。因此,一个真正明智的实现将考虑到这一点。