Loading

AVR汇编(四):数据传送指令

AVR汇编(四):数据传送指令

AVR指令主要分为五类:算术和逻辑指令、分支指令、位操作指令、数据传送指令、MCU控制指令,今天我们先来认识其中最常用的数据传送指令。

汇编程序的编写、编译和调试

学习任何技术都离不开实践,汇编语言也是如此。在正式学习指令前,我们先来体验一下汇编程序从编写到编译,再到调试的整个过程。

伪指令

根据百度百科:伪指令(Pseudo Instruction)是用于对汇编过程进行控制的指令,该类指令并不是可执行指令,没有机器代码,只用于汇编过程中为汇编程序提供汇编信息。

下面是几个常用的伪指令:

伪指令 说明 举例
.section 定义一个段 .section .text
.section .data
.global 定义一个全局符号 .global _start
.byte 定义一个字节数据 .byte 0x01
.word 定义一个字数据 .word 0x3412
.ascii 定义一个字符串数据 .ascii "hello"
.align 设置对齐方式 .align 4
.equ 定义一个符号常量,类似于C宏定义 .equ INT8_MAX, 0xFF

第一个汇编程序

.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

image.png

在GDB窗口中,可以输入 s 进行单步执行。

为了观察 PINBDDRBPORTB 寄存器的值,可以借助 x/<n/f/u> <addr> 命令,其中 n 表示要查看的值的个数; f 指定显示格式,如果要十六进制显示,这里就要指定 xu 表示值的单位,如果单位是字节,这里就要指定 b 。这条命令的具体使用方法可以通过 help x 命令查看。

这里我们查看从I/O地址0x03开始的3个字节:

x/3xb 0x03      # 注意,这里的地址是错误的!

结果如下:

image.png

发现读取的值并不符合我们的预期,这是因为上面命令中的地址设置错了,有两个因素:

  1. PINBDDRBPORTB 在I/O空间的地址是0x03开始,而在数据空间中的地址需要加上0x20;
  2. AVR的程序空间和数据空间是分别独立编址的,因此地址存在重叠情况。通过 avr-readelf -S hello.elf 查看,可以发现 .data 段的地址是从0x800100开始的,而实际的SRAM地址是从 0x0100 开始的,因此可以知道elf文件中数据空间的地址还需要加上0x800000,如果不加,则代表的是 .text 段(Flash)的地址。

image.png

通过上面的分析,将命令中的地址改为0x800023即可正确查看 PINBDDRBPORTB 中的内容:

x/3xb 0x800023

结果如下:

image.png

这样显示的结果与我们的程序逻辑是一致的。

数据传送指令

由于AVR具有多种寻址方式,因此数据传送指令也对应有多种。

空间 指令
寄存器堆 MOV
数据空间 LD / ST
程序空间 LPM / SPM
I/O空间 IN / OUT
栈空间 PUSH / POP

一般而言,AVR指令如果有两个操作数,则第一个是目的操作数,第二个是源操作数。

MOV

image.png

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

image.png

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

image.png

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

image.png

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

image.png

IN / OUT 用于从I/O空间读入数据到寄存器/向I/O空间写入寄存器中的数据,注意 P 为I/O空间的地址,此命令不能访问扩展I/O空间。

例如:

OUT DDRB, 0x00    ; DDRB = 0xAA
IN  R0, DDRB      ; R0 = 0xAA

PUSH / POP

image.png

PUSH / POP 用于将数据压入/弹出栈,使用时需要注意SP的初始值要设置正确(AVR是空减栈),并要避免出现栈溢出的情况。

例如:

LDI  R16, 0xA5    ; R16 = 0xA5
PUSH R16          ; (SP) = 0xA5, SP -= 1
POP  R0           ; R0 = 0xA5, SP += 1

参考资料

  1. ATmega328P Datasheet
  2. AVR Instruction Set Manual
  3. 百度百科 - 伪指令
posted @ 2023-08-11 21:48  chinjinyu  阅读(616)  评论(0编辑  收藏  举报