• 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 指定显示格式,如果要十六进制显示,这里就要指定 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. 百度百科 - 伪指令
  • 相关阅读:
    数组与链表算法-单向链表算法
    Spring MVC响应处理
    Java固定资产管理系统源码
    Lua - 替换字符串中的特殊字符
    [附源码]java毕业设计潮流服饰网店平台
    Linux下gcc和gdb的基本使用
    网络技术二十一:ACL包过滤
    oracle-AWR报告生成方法
    C语言oj题
    对比Vue2和Vue3的自定义指令
  • 原文地址:https://www.cnblogs.com/chinjinyu/p/17624021.html