在 AVR 组件中创建和寻址数组(使用 ATMega8535)
Creation and addressing arrays in AVR Assembly (Using the ATMega8535)
我在使用 Atmel ATMega8535 的指令集创建和寻址纯汇编创建的数组时遇到了问题。
目前我的理解如下:
- 数组包含长度相等的连续数据。
- 数组的创建涉及定义数组的开始和结束位置(很像堆栈)。
- 您可以通过添加数组基地址的偏移量来寻址数组中的索引。
我特别想做的是创建一个 8 位整数的一维数组,并在初始化期间填充预定义值,它不必写入,仅在需要时才处理。问题最终还是在于无法将逻辑转化为汇编代码。
我尝试使用以下书籍的支持来做到这一点,但进展甚微:
- 需要一些汇编:Timothy S Margush 的 AVR 微控制器汇编语言编程
- 开始使用...AVR 微控制器作者:Peter Sharpe
如有任何帮助、建议或更多资源,我们将不胜感激。
代码应如下所示:
.section .text
.global main
main:
ldi r30,lo8(data)
ldi r31,hi8(data)
ldd r24,Z+3
sts output,r24
ld r24,Z
sts output,r24
ldi r24,0
ldi r25,0
ret
.global data
.data
data:
.byte 1, 2, 3, 4
.comm output,1,1
说明
对于以前使用 GNU 工具链在汇编程序中编程的人来说,有些课程甚至可以转移到不熟悉的指令集:
- 您为带有汇编指令
.byte 1, 2, 3, 4
、.word 1, 2
(.word
是 AVR 的 16 位)或 .space 100
的数组保留 space。 =81=]
- 在学习新的指令集时,编写C程序并要求C编译器生成汇编输出。在阅读汇编程序代码时,为指令集找到一个好的汇编程序编程参考。
在下面应用这个技巧。
byte-array.c
/* volatile our code doesn't get optimized out even when compiler optimization is on */
volatile char output;
char data[] = { 1, 2, 3, 4 };
int main(void)
{
output = data[3];
output = data[0];
return 0;
}
从 C 生成汇编程序
avr-gcc -mmcu=atmega8 -Wall -Os -S byte-array.c
这将生成汇编文件 byte-array.s
。
byte-array.s
.file "byte-array.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.section .text.startup,"ax",@progbits
.global main
.type main, @function
main:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r30,lo8(data)
ldi r31,hi8(data)
ldd r24,Z+3
sts output,r24
ld r24,Z
sts output,r24
ldi r24,0
ldi r25,0
ret
.size main, .-main
.global data
.data
.type data, @object
.size data, 4
data:
.byte 1
.byte 2
.byte 3
.byte 4
.comm output,1,1
.ident "GCC: (Fedora 4.9.2-1.fc21) 4.9.2"
.global __do_copy_data
.global __do_clear_bss
阅读此explanation of Pointer Registers,了解AVR 指令集如何使用r30
、r31
寄存器对作为指针寄存器Z
。阅读 ld
、st
、ldi
、ldd
、sts
和 std
指令。
实施说明
如果你link程序然后反汇编它:
avr-gcc -mmcu=atmega8 -Os byte-array.c -o byte-array.elf
avr-objdump -d byte-array.elf
00000000 <__vectors>:
0: 12 c0 rjmp .+36 ; 0x26 <__ctors_end>
2: 2c c0 rjmp .+88 ; 0x5c <__bad_interrupt>
4: 2b c0 rjmp .+86 ; 0x5c <__bad_interrupt>
6: 2a c0 rjmp .+84 ; 0x5c <__bad_interrupt>
8: 29 c0 rjmp .+82 ; 0x5c <__bad_interrupt>
a: 28 c0 rjmp .+80 ; 0x5c <__bad_interrupt>
c: 27 c0 rjmp .+78 ; 0x5c <__bad_interrupt>
e: 26 c0 rjmp .+76 ; 0x5c <__bad_interrupt>
10: 25 c0 rjmp .+74 ; 0x5c <__bad_interrupt>
12: 24 c0 rjmp .+72 ; 0x5c <__bad_interrupt>
14: 23 c0 rjmp .+70 ; 0x5c <__bad_interrupt>
16: 22 c0 rjmp .+68 ; 0x5c <__bad_interrupt>
18: 21 c0 rjmp .+66 ; 0x5c <__bad_interrupt>
1a: 20 c0 rjmp .+64 ; 0x5c <__bad_interrupt>
1c: 1f c0 rjmp .+62 ; 0x5c <__bad_interrupt>
1e: 1e c0 rjmp .+60 ; 0x5c <__bad_interrupt>
20: 1d c0 rjmp .+58 ; 0x5c <__bad_interrupt>
22: 1c c0 rjmp .+56 ; 0x5c <__bad_interrupt>
24: 1b c0 rjmp .+54 ; 0x5c <__bad_interrupt>
00000026 <__ctors_end>:
26: 11 24 eor r1, r1
28: 1f be out 0x3f, r1 ; 63
2a: cf e5 ldi r28, 0x5F ; 95
2c: d4 e0 ldi r29, 0x04 ; 4
2e: de bf out 0x3e, r29 ; 62
30: cd bf out 0x3d, r28 ; 61
00000032 <__do_copy_data>:
32: 10 e0 ldi r17, 0x00 ; 0
34: a0 e6 ldi r26, 0x60 ; 96
36: b0 e0 ldi r27, 0x00 ; 0
38: e4 e8 ldi r30, 0x84 ; 132
3a: f0 e0 ldi r31, 0x00 ; 0
3c: 02 c0 rjmp .+4 ; 0x42 <__SREG__+0x3>
3e: 05 90 lpm r0, Z+
40: 0d 92 st X+, r0
42: ac 36 cpi r26, 0x6C ; 108
44: b1 07 cpc r27, r17
46: d9 f7 brne .-10 ; 0x3e <__SP_H__>
00000048 <__do_clear_bss>:
48: 10 e0 ldi r17, 0x00 ; 0
4a: ac e6 ldi r26, 0x6C ; 108
4c: b0 e0 ldi r27, 0x00 ; 0
4e: 01 c0 rjmp .+2 ; 0x52 <.do_clear_bss_start>
00000050 <.do_clear_bss_loop>:
50: 1d 92 st X+, r1
00000052 <.do_clear_bss_start>:
52: ad 36 cpi r26, 0x6D ; 109
54: b1 07 cpc r27, r17
56: e1 f7 brne .-8 ; 0x50 <.do_clear_bss_loop>
58: 02 d0 rcall .+4 ; 0x5e <main>
5a: 12 c0 rjmp .+36 ; 0x80 <_exit>
0000005c <__bad_interrupt>:
5c: d1 cf rjmp .-94 ; 0x0 <__vectors>
0000005e <main>: ...
00000080 <_exit>:
80: f8 94 cli
00000082 <__stop_program>:
82: ff cf rjmp .-2 ; 0x82 <__stop_program>
可以看到avr-gcc
自动生成启动码,包括:
- 中断向量 (
__vectors
),它使用 rjmp
跳转到中断服务例程。
- 初始化状态寄存器
SREG
和堆栈指针 SPL/SPH
(__ctors_end
)
- 将数据段内容从FLASH复制到RAM,用于初始化、可写的全局变量(
__do_copy_data
)
- 清除未初始化的可写全局变量的 BSS 段(
__do_clear_bss
等)
- 调用我们的
main()
函数
- 调用
_exit()
如果 main()
曾经 returns
_exit()
只是一个cli
来禁用中断
- 和无限循环 (
__stop_program
)
如果您的数组是只读的,则不需要将其复制到 RAM。你可以
将其保存在 Flash 中,并在需要时从那里读取。这会救你
宝贵的 RAM,以较慢的访问为代价(从 RAM 读取是 2 个周期,
从闪存读取是 3 个周期)。
你可以这样声明你的数组:
.global my_array
.type my_array, @object
my_array:
.byte 12, 34, 56, 78
然后,要读取数组的一个成员,您必须计算:
adress of member = array base address + member index
如果您的成员超过一个字节,您还必须乘以
按大小索引,但这里不是这种情况。然后,你把
Z 寄存器中所需成员的地址并发出 lpm
操作说明。这是实现此逻辑的函数:
.global read_data
; input: r24 = array index, r1 = 0
; output: r24 = array value
; clobbers: r30, r31
read_data:
ldi r30, lo8(my_array) ; load Z = address of my_array
ldi r31, hi8(my_array) ; ...high byte also
add r30, r24 ; add the array index
adc r31, r1 ; ...and add 0 to propagate the carry
lpm r24, Z
ret
@scott 建议你先用C写,再看生成的
部件。我认为这个建议很好,让我们遵循它:
#include <stdint.h>
__flash const uint8_t my_array[] = {12, 34, 56, 78};
uint8_t read_data(uint8_t index)
{
return my_array[index];
}
标识“命名地址space”的__flash
关键字是一个嵌入式
C 扩展 支持
gcc。这
生成的程序集与之前的程序集略有不同:相反
计算 base_address + index
,gcc 做 index − (−base_address)
:
read_data:
mov r30, r24 ; load Z = array index
ldi r31, 0 ; ...high byte of index is 0
subi r30, lo8(-(my_array)) ; subtract -(address of my array)
sbci r31, hi8(-(my_array)) ; ...high byte also
lpm r24, Z
ret
这与以前的手卷组装一样有效,除了
它不需要将 r1 寄存器初始化为零。但
无论如何,保持 r1 为零是 gcc ABI 的一部分,所以它应该不
区别。
链接器的作用
本节旨在回答评论中的问题:我们如何才能
如果我们不知道它的地址访问数组?答案是:我们访问
它的名字,就像上面的代码片段一样。选择决赛
数组的地址,以及用适当的名称替换名称
地址,是链接器的工作。
组装(使用avr-gcc -c
)和拆卸(使用avr-objdump -d
)
第一个代码片段给出了这个:
my_array.o, section .text:
00000000 <my_array>:
0: 0c 22 38 4e ."8N
如果我们从 C 编译,gcc 会将数组放在
.progmem.data 节而不是 .text,但差别不大。
数字“0c 22 38 4e”是十六进制的数组内容。那些角色
右边是 ASCII 等价物,'.' 是占位符
非打印字符。
目标文件也带有这个符号table,显示为avr-nm
:
my_array.o:
00000000 T my_array
表示符号“my_array”已被定义为引用偏移量 0
进入该对象的 .text 部分(由“T”表示)。
组装和反汇编第二个代码片段给出了这个:
read_data.o, section .text:
00000000 <read_data>:
0: e0 e0 ldi r30, 0x00
2: f0 e0 ldi r31, 0x00
4: e8 0f add r30, r24
6: f1 1d adc r31, r1
8: 84 91 lpm r24, Z
a: 08 95 ret
反汇编与实际源码对比,可见
汇编程序将 my_array 的地址替换为 0x00,即
几乎可以保证是错误的。但它也给链接器留下了注释
“搬迁记录”的形式,如avr-objdump -r
:
read_data.o, RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000000 R_AVR_LO8_LDI my_array
00000002 R_AVR_HI8_LDI my_array
这告诉链接器 ldi
指令在偏移量 0x00 和
0x02 用于加载低字节和高字节(分别)
my_array 的最终地址。目标文件也带有这个
符号 table:
read_data.o:
U my_array
00000000 T read_data
其中“U”行表示文件使用了一个名为
“my_array”。
使用 suitable main() 将这些部分链接在一起,产生一个二进制文件
包含来自 avr-lbc 的 C 运行时以及我们的代码:
0000003c <my_array>:
3c: 0c 22 38 4e ."8N
00000040 <read_data>:
40: ec e3 ldi r30, 0x3C
42: f0 e0 ldi r31, 0x00
44: e8 0f add r30, r24
46: f1 1d adc r31, r1
48: 84 91 lpm r24, Z
4a: 08 95 ret
应该注意的是,链接器不仅移动了这些片段
对于他们的最终地址,它还修复了 ldi
的参数
指令,以便它们现在指向 my_array.
的正确地址
我在使用 Atmel ATMega8535 的指令集创建和寻址纯汇编创建的数组时遇到了问题。
目前我的理解如下:
- 数组包含长度相等的连续数据。
- 数组的创建涉及定义数组的开始和结束位置(很像堆栈)。
- 您可以通过添加数组基地址的偏移量来寻址数组中的索引。
我特别想做的是创建一个 8 位整数的一维数组,并在初始化期间填充预定义值,它不必写入,仅在需要时才处理。问题最终还是在于无法将逻辑转化为汇编代码。
我尝试使用以下书籍的支持来做到这一点,但进展甚微:
- 需要一些汇编:Timothy S Margush 的 AVR 微控制器汇编语言编程
- 开始使用...AVR 微控制器作者:Peter Sharpe
如有任何帮助、建议或更多资源,我们将不胜感激。
代码应如下所示:
.section .text
.global main
main:
ldi r30,lo8(data)
ldi r31,hi8(data)
ldd r24,Z+3
sts output,r24
ld r24,Z
sts output,r24
ldi r24,0
ldi r25,0
ret
.global data
.data
data:
.byte 1, 2, 3, 4
.comm output,1,1
说明
对于以前使用 GNU 工具链在汇编程序中编程的人来说,有些课程甚至可以转移到不熟悉的指令集:
- 您为带有汇编指令
.byte 1, 2, 3, 4
、.word 1, 2
(.word
是 AVR 的 16 位)或.space 100
的数组保留 space。 =81=] - 在学习新的指令集时,编写C程序并要求C编译器生成汇编输出。在阅读汇编程序代码时,为指令集找到一个好的汇编程序编程参考。
在下面应用这个技巧。
byte-array.c
/* volatile our code doesn't get optimized out even when compiler optimization is on */
volatile char output;
char data[] = { 1, 2, 3, 4 };
int main(void)
{
output = data[3];
output = data[0];
return 0;
}
从 C 生成汇编程序
avr-gcc -mmcu=atmega8 -Wall -Os -S byte-array.c
这将生成汇编文件 byte-array.s
。
byte-array.s
.file "byte-array.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.section .text.startup,"ax",@progbits
.global main
.type main, @function
main:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r30,lo8(data)
ldi r31,hi8(data)
ldd r24,Z+3
sts output,r24
ld r24,Z
sts output,r24
ldi r24,0
ldi r25,0
ret
.size main, .-main
.global data
.data
.type data, @object
.size data, 4
data:
.byte 1
.byte 2
.byte 3
.byte 4
.comm output,1,1
.ident "GCC: (Fedora 4.9.2-1.fc21) 4.9.2"
.global __do_copy_data
.global __do_clear_bss
阅读此explanation of Pointer Registers,了解AVR 指令集如何使用r30
、r31
寄存器对作为指针寄存器Z
。阅读 ld
、st
、ldi
、ldd
、sts
和 std
指令。
实施说明
如果你link程序然后反汇编它:
avr-gcc -mmcu=atmega8 -Os byte-array.c -o byte-array.elf
avr-objdump -d byte-array.elf
00000000 <__vectors>:
0: 12 c0 rjmp .+36 ; 0x26 <__ctors_end>
2: 2c c0 rjmp .+88 ; 0x5c <__bad_interrupt>
4: 2b c0 rjmp .+86 ; 0x5c <__bad_interrupt>
6: 2a c0 rjmp .+84 ; 0x5c <__bad_interrupt>
8: 29 c0 rjmp .+82 ; 0x5c <__bad_interrupt>
a: 28 c0 rjmp .+80 ; 0x5c <__bad_interrupt>
c: 27 c0 rjmp .+78 ; 0x5c <__bad_interrupt>
e: 26 c0 rjmp .+76 ; 0x5c <__bad_interrupt>
10: 25 c0 rjmp .+74 ; 0x5c <__bad_interrupt>
12: 24 c0 rjmp .+72 ; 0x5c <__bad_interrupt>
14: 23 c0 rjmp .+70 ; 0x5c <__bad_interrupt>
16: 22 c0 rjmp .+68 ; 0x5c <__bad_interrupt>
18: 21 c0 rjmp .+66 ; 0x5c <__bad_interrupt>
1a: 20 c0 rjmp .+64 ; 0x5c <__bad_interrupt>
1c: 1f c0 rjmp .+62 ; 0x5c <__bad_interrupt>
1e: 1e c0 rjmp .+60 ; 0x5c <__bad_interrupt>
20: 1d c0 rjmp .+58 ; 0x5c <__bad_interrupt>
22: 1c c0 rjmp .+56 ; 0x5c <__bad_interrupt>
24: 1b c0 rjmp .+54 ; 0x5c <__bad_interrupt>
00000026 <__ctors_end>:
26: 11 24 eor r1, r1
28: 1f be out 0x3f, r1 ; 63
2a: cf e5 ldi r28, 0x5F ; 95
2c: d4 e0 ldi r29, 0x04 ; 4
2e: de bf out 0x3e, r29 ; 62
30: cd bf out 0x3d, r28 ; 61
00000032 <__do_copy_data>:
32: 10 e0 ldi r17, 0x00 ; 0
34: a0 e6 ldi r26, 0x60 ; 96
36: b0 e0 ldi r27, 0x00 ; 0
38: e4 e8 ldi r30, 0x84 ; 132
3a: f0 e0 ldi r31, 0x00 ; 0
3c: 02 c0 rjmp .+4 ; 0x42 <__SREG__+0x3>
3e: 05 90 lpm r0, Z+
40: 0d 92 st X+, r0
42: ac 36 cpi r26, 0x6C ; 108
44: b1 07 cpc r27, r17
46: d9 f7 brne .-10 ; 0x3e <__SP_H__>
00000048 <__do_clear_bss>:
48: 10 e0 ldi r17, 0x00 ; 0
4a: ac e6 ldi r26, 0x6C ; 108
4c: b0 e0 ldi r27, 0x00 ; 0
4e: 01 c0 rjmp .+2 ; 0x52 <.do_clear_bss_start>
00000050 <.do_clear_bss_loop>:
50: 1d 92 st X+, r1
00000052 <.do_clear_bss_start>:
52: ad 36 cpi r26, 0x6D ; 109
54: b1 07 cpc r27, r17
56: e1 f7 brne .-8 ; 0x50 <.do_clear_bss_loop>
58: 02 d0 rcall .+4 ; 0x5e <main>
5a: 12 c0 rjmp .+36 ; 0x80 <_exit>
0000005c <__bad_interrupt>:
5c: d1 cf rjmp .-94 ; 0x0 <__vectors>
0000005e <main>: ...
00000080 <_exit>:
80: f8 94 cli
00000082 <__stop_program>:
82: ff cf rjmp .-2 ; 0x82 <__stop_program>
可以看到avr-gcc
自动生成启动码,包括:
- 中断向量 (
__vectors
),它使用rjmp
跳转到中断服务例程。 - 初始化状态寄存器
SREG
和堆栈指针SPL/SPH
(__ctors_end
) - 将数据段内容从FLASH复制到RAM,用于初始化、可写的全局变量(
__do_copy_data
) - 清除未初始化的可写全局变量的 BSS 段(
__do_clear_bss
等) - 调用我们的
main()
函数 - 调用
_exit()
如果main()
曾经 returns _exit()
只是一个cli
来禁用中断- 和无限循环 (
__stop_program
)
如果您的数组是只读的,则不需要将其复制到 RAM。你可以 将其保存在 Flash 中,并在需要时从那里读取。这会救你 宝贵的 RAM,以较慢的访问为代价(从 RAM 读取是 2 个周期, 从闪存读取是 3 个周期)。
你可以这样声明你的数组:
.global my_array
.type my_array, @object
my_array:
.byte 12, 34, 56, 78
然后,要读取数组的一个成员,您必须计算:
adress of member = array base address + member index
如果您的成员超过一个字节,您还必须乘以
按大小索引,但这里不是这种情况。然后,你把
Z 寄存器中所需成员的地址并发出 lpm
操作说明。这是实现此逻辑的函数:
.global read_data
; input: r24 = array index, r1 = 0
; output: r24 = array value
; clobbers: r30, r31
read_data:
ldi r30, lo8(my_array) ; load Z = address of my_array
ldi r31, hi8(my_array) ; ...high byte also
add r30, r24 ; add the array index
adc r31, r1 ; ...and add 0 to propagate the carry
lpm r24, Z
ret
@scott 建议你先用C写,再看生成的 部件。我认为这个建议很好,让我们遵循它:
#include <stdint.h>
__flash const uint8_t my_array[] = {12, 34, 56, 78};
uint8_t read_data(uint8_t index)
{
return my_array[index];
}
标识“命名地址space”的__flash
关键字是一个嵌入式
C 扩展 支持
gcc。这
生成的程序集与之前的程序集略有不同:相反
计算 base_address + index
,gcc 做 index − (−base_address)
:
read_data:
mov r30, r24 ; load Z = array index
ldi r31, 0 ; ...high byte of index is 0
subi r30, lo8(-(my_array)) ; subtract -(address of my array)
sbci r31, hi8(-(my_array)) ; ...high byte also
lpm r24, Z
ret
这与以前的手卷组装一样有效,除了 它不需要将 r1 寄存器初始化为零。但 无论如何,保持 r1 为零是 gcc ABI 的一部分,所以它应该不 区别。
链接器的作用
本节旨在回答评论中的问题:我们如何才能 如果我们不知道它的地址访问数组?答案是:我们访问 它的名字,就像上面的代码片段一样。选择决赛 数组的地址,以及用适当的名称替换名称 地址,是链接器的工作。
组装(使用avr-gcc -c
)和拆卸(使用avr-objdump -d
)
第一个代码片段给出了这个:
my_array.o, section .text:
00000000 <my_array>:
0: 0c 22 38 4e ."8N
如果我们从 C 编译,gcc 会将数组放在 .progmem.data 节而不是 .text,但差别不大。 数字“0c 22 38 4e”是十六进制的数组内容。那些角色 右边是 ASCII 等价物,'.' 是占位符 非打印字符。
目标文件也带有这个符号table,显示为avr-nm
:
my_array.o:
00000000 T my_array
表示符号“my_array”已被定义为引用偏移量 0 进入该对象的 .text 部分(由“T”表示)。
组装和反汇编第二个代码片段给出了这个:
read_data.o, section .text:
00000000 <read_data>:
0: e0 e0 ldi r30, 0x00
2: f0 e0 ldi r31, 0x00
4: e8 0f add r30, r24
6: f1 1d adc r31, r1
8: 84 91 lpm r24, Z
a: 08 95 ret
反汇编与实际源码对比,可见
汇编程序将 my_array 的地址替换为 0x00,即
几乎可以保证是错误的。但它也给链接器留下了注释
“搬迁记录”的形式,如avr-objdump -r
:
read_data.o, RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000000 R_AVR_LO8_LDI my_array
00000002 R_AVR_HI8_LDI my_array
这告诉链接器 ldi
指令在偏移量 0x00 和
0x02 用于加载低字节和高字节(分别)
my_array 的最终地址。目标文件也带有这个
符号 table:
read_data.o:
U my_array
00000000 T read_data
其中“U”行表示文件使用了一个名为 “my_array”。
使用 suitable main() 将这些部分链接在一起,产生一个二进制文件 包含来自 avr-lbc 的 C 运行时以及我们的代码:
0000003c <my_array>:
3c: 0c 22 38 4e ."8N
00000040 <read_data>:
40: ec e3 ldi r30, 0x3C
42: f0 e0 ldi r31, 0x00
44: e8 0f add r30, r24
46: f1 1d adc r31, r1
48: 84 91 lpm r24, Z
4a: 08 95 ret
应该注意的是,链接器不仅移动了这些片段
对于他们的最终地址,它还修复了 ldi
的参数
指令,以便它们现在指向 my_array.