AVR汇编(四):数据传送指令
AVR指令主要分为五类:算术和逻辑指令、分支指令、位操作指令、数据传送指令、MCU控制指令,今天我们先来认识其中最常用的数据传送指令。
汇编程序的编写、编译和调试
学习任何技术都离不开实践,汇编语言也是如此。在正式学习指令前,我们先来体验一下汇编程序从编写到编译,再到调试的整个过程。
伪指令
根据百度百科:伪指令(Pseudo Instruction)是用于对汇编过程进行控制的指令,该类指令并不是可执行指令,没有机器代码,只用于汇编过程中为汇编程序提供汇编信息。
下面是几个常用的伪指令:
第一个汇编程序
.equ PINB, 0x03
.equ DDRB, 0x04
.equ PORTB, 0x05
.section .text ; 定义text段
.global _start ; 定义一个全局符号_start
_start:
LDI R16, 0x20 ; R16 = 0x20
OUT DDRB, R16 ; 设置PB5为输出
OUT PORTB, R16 ; PB5初始输出高电平
loop:
OUT PINB, R16 ; 翻转PB5电平
RJMP loop ; 跳转到loop处继续执行
上述程序实现的功能很简单,就是不断翻转PB5的电平状态。
使用下面的命令进行编译,生成elf文件:
avr-gcc -mmcu=atmega328p -x assembler-with-cpp -g -Og -Wall -c -o hello.o hello.s
avr-gcc -mmcu=atmega328p -nostartfiles -o hello.elf hello.o
其中, -x assembler-with-cpp
表示编译汇编程序, -nostartfiles
表示不添加默认启动文件,启动文件的作用是初始化MCU,创建C语言运行环境,由于这里编写的是汇编程序,所以不需要它,否则编译时会提示找不到 main
函数。
为了以后每次重新编译的时候不用都输一遍命令,可以写一个 Makefile
文件:
.PHONY: all clean
TARGET := hello
all: $(TARGET).elf
%.o: %.s
avr-gcc -mmcu=atmega328p -x assembler-with-cpp -g -Og -Wall -c -o $@ $<
$(TARGET).elf: $(TARGET).o
avr-gcc -mmcu=atmega328p -nostartfiles -o $@ $<
clean:
rm -f *.o $(TARGET).elf
调试程序
使用 simavr
对上面生成的elf文件进行仿真:
simavr -f 16000000 -m atmega328p --gdb hello.elf
为了方便,可以在 Makefile
中添加一个 run
伪目标,将上面的命令添加进去:
.PHONY: all clean run
...
run: $(TARGET).elf
simavr -f 16000000 -m atmega328p --gdb $<
之后需要仿真时,直接执行 make run
即可。
使用 avr-gdb
对程序进行调试, simavr
的GDB端口是 1234
:
avr-gdb -ex "target remote localhost:1234" -ex "layout split" -q --tui hello.elf
在GDB窗口中,可以输入 s
进行单步执行。
为了观察 PINB
、 DDRB
、 PORTB
寄存器的值,可以借助 x/<n/f/u> <addr>
命令,其中 n
表示要查看的值的个数; f
指定显示格式,如果要十六进制显示,这里就要指定 x
; u
表示值的单位,如果单位是字节,这里就要指定 b
。这条命令的具体使用方法可以通过 help x
命令查看。
这里我们查看从I/O地址0x03开始的3个字节:
x/3xb 0x03 # 注意,这里的地址是错误的!
结果如下:
发现读取的值并不符合我们的预期,这是因为上面命令中的地址设置错了,有两个因素:
PINB
、DDRB
、PORTB
在I/O空间的地址是0x03开始,而在数据空间中的地址需要加上0x20;- AVR的程序空间和数据空间是分别独立编址的,因此地址存在重叠情况。通过
avr-readelf -S hello.elf
查看,可以发现.data
段的地址是从0x800100开始的,而实际的SRAM地址是从0x0100
开始的,因此可以知道elf文件中数据空间的地址还需要加上0x800000,如果不加,则代表的是.text
段(Flash)的地址。
通过上面的分析,将命令中的地址改为0x800023即可正确查看 PINB
、 DDRB
、 PORTB
中的内容:
x/3xb 0x800023
结果如下:
这样显示的结果与我们的程序逻辑是一致的。
数据传送指令
由于AVR具有多种寻址方式,因此数据传送指令也对应有多种。
一般而言,AVR指令如果有两个操作数,则第一个是目的操作数,第二个是源操作数。
MOV
MOV
指令用于寄存器之间的数据传送(一个字节),后缀如果加 W
表示传送一个字的数据。
例如:
LDI R16, 0x10 ; R16 = 0x10
MOV R0, R16 ; R0 = 0x10
LDI R16, 0x20 ; R16 = 0x20
LDI R17, 0x30 ; R17 = 0x30
MOVW R0, R16 ; R0 = 0x20, R1 = 0x30
LD
LD
指令用于将数据从数据空间加载到寄存器中,后缀加 I
表示加载立即数,加 D
表示偏移量寻址,加 S
表示直接寻址。
X
/ Y
/ Z
寄存器可以用于间接寻址,如果前缀加 -
,表示执行操作前寄存器的值自减一,如果后缀加 +
,表示执行操作后寄存器的值自加一。
Y
/ Z
寄存器可以用于偏移量寻址(注意不包括 X
寄存器),后面加 +q
表示偏移量为 q
。
例如:
LDI R16, 0xAA ; R16 = 0xAA
LDI XL, 0x00
LDI XH, 0x01 ; X = 0x0100
ST X, R16 ; (0x0100) = 0xAA
LD R0, X+ ; R0 = 0xAA, X = 0x0101
LDI ZL, 0xF1
LDI ZH, 0x00 ; Z = 0x00F1
LDD R1, Z+0xF ; R1 = 0xAA
LDS R2, 0x0100 ; R2 = 0xAA
ST
ST
指令用于将数据从寄存器写入到数据空间中,后缀加 D
/ S
的意义同 LD
,注意 ST
不支持立即寻址,即没有 STI
这样的指令!
例如:
LDI R16, 0x55 ; R16 = 0x55
LDI XL, 0x80
LDI XH, 0x01 ; X = 0x0180
ST X, R16 ; (0x0180) = 0x55
LD R0, X ; R0 = 0x55
LDI R16, 0xAA ; R16 = 0xAA
LDI ZL, 0x50
LDI ZH, 0x01 ; Z = 0x0150
STD Z+0x30, R16 ; (0x0180) = 0xAA
LD R1, X ; R1 = 0xAA
LDI R16, 0xA5 ; R16 = 0xA5
STS 0x0180, R16 ; (0x0180) = 0xA5
LD R2, X ; R2 = 0xA5
LPM
/ SPM
LPM
/ SPM
指令用于将数据从程序空间加载到寄存器/从寄存器写入到程序空间。
例如:
LDI ZL, lo8(const)
LDI ZH, hi8(const) ; Z = const
LPM R0, Z+ ; R0 = 0xA5, Z = CONST + 1
LPM R1, Z+ ; R1 = 0x5A, Z = CONST + 2
LPM R2, Z+ ; R2 = 0x55, Z = CONST + 3
LPM R3, Z ; R3 = 0xAA
const:
.byte 0xA5
.byte 0x5A
.word 0xAA55
SPM
指令的用法较为特殊,后面有机会再来介绍。
IN
/ OUT
IN
/ OUT
用于从I/O空间读入数据到寄存器/向I/O空间写入寄存器中的数据,注意 P
为I/O空间的地址,此命令不能访问扩展I/O空间。
例如:
OUT DDRB, 0x00 ; DDRB = 0xAA
IN R0, DDRB ; R0 = 0xAA
PUSH
/ POP
PUSH
/ POP
用于将数据压入/弹出栈,使用时需要注意SP的初始值要设置正确(AVR是空减栈),并要避免出现栈溢出的情况。
例如:
LDI R16, 0xA5 ; R16 = 0xA5
PUSH R16 ; (SP) = 0xA5, SP -= 1
POP R0 ; R0 = 0xA5, SP += 1