←第二部分。入门
第4部分。编程外围设备和处理中断→
AVR微控制器的汇编代码生成器库
第3部分。间接寻址和流控制
在上一部分中,我们详细介绍了如何使用8位寄存器变量。 如果您错过了上一篇文章,建议您阅读。 在其中,您可以找到该库的链接,以亲自尝试本文中的示例。 对于那些较早下载该库的人,我建议下载最新版本,因为该库会不断更新,并且某些示例可能在旧版本的库中不起作用。
不幸的是,先前考虑的寄存器变量的位深度显然不足以用作存储器指针。 因此,在直接进行指针讨论之前,我们考虑另一类数据描述。 AVR Mega架构中的大多数命令都设计为仅与寄存器操作数一起使用,也就是说,两个操作数及其结果均为8位大小。 但是,在许多操作中,两个连续放置的RON寄存器被视为单个16位寄存器。 这样的操作很少,主要集中在使用指针上。
从库语法的角度来看,使用寄存器对与使用寄存器变量几乎相同。 考虑一个小例子,我们尝试使用寄存器对。 为了节省这里和下面的空间,我们将仅在有必要解释代码生成某些功能的地方给出执行结果。
var m = new Mega328(); var dr1 = m.DREG(); var dr2 = m.DREG(); dr1.Load(0xAA55); dr2.Load(0x55AA); dr1++; dr1--; dr1 += 0x100; dr1 += dr2; dr2 *= dr1; dr2 /= dr1; var t = AVRASM.Text(m);
在此示例中,我们使用DREG()命令声明了两个位于寄存器对中的2字节变量。 使用以下命令,我们为他们分配了初始值并执行了一系列算术运算。 从示例中可以看到,使用寄存器对的语法与使用常规寄存器的语法基本相同。 寄存器对也可以视为由两个独立寄存器组成的变量。 通过High属性(作为一个8位寄存器访问高8位),以及Low属性(用于访问低8位),以两个8位寄存器的集合的形式访问该寄存器。 该代码将如下所示
var m = new Mega328(); var dr1 = m.DREG(); dr1.Load(0xAA55); dr1.Low--; dr1.High += dr1.Low; var t = AVRASM.Text(m);
从示例中可以看到,我们可以将High和Low用作独立的寄存器变量,包括在它们之间执行各种算术和逻辑运算。
现在我们已经弄清楚了双精度变量,我们可以开始描述如何使用内存中的变量。 该库允许您使用8个16位变量和任意长度的字节数组。 考虑一个为RAM中的变量分配空间的示例。
var m = new Mega328(); var bt = m.BYTE();
让我们看看发生了什么。
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DSEG L0002: .BYTE 16 L0001: .BYTE 2 L0000: .BYTE 1
在数据定义部分,我们有一个内存分配。 请注意,分配顺序与变量的声明不同。 这不是巧合。 变量的内存分配是在按照以下标准以降序排序之后进行的(以重要性从高到低的顺序排列):2级的最大除数→分配的内存的大小。 这意味着,如果我们要分配大小分别为64、48、40和16个字节的4个数组,则无论声明顺序如何,分配顺序都将如下所示:
长度64-度的最大除数2 = 64
长度48-2的最大除数倍数= 16
长度16-2的最大除数倍数= 16
长度40-度的最大除数2 = 8
这样做是为了简化阵列边界的控制。
并在使用指针的操作中减少代码的大小。 我们无法直接对内存中的变量执行任何操作,因此我们所能做的就是读/写寄存器变量。 使用内存中变量的最简单方法是直接寻址。
var m = new Mega328(); var bt = m.BYTE();
在此示例中,我们在内存中声明了一个变量,并声明了一个寄存器变量。 之后,我们为变量分配了值0x55,并将其写入内存中的变量。 然后擦除并恢复原状。
要使用数组元素,我们使用以下语法
var rr = m.REG(); var arr = m.ARRAY(10); rr.MLoad(arr[5]);
数组中元素的编号从0开始。因此,在以上示例中,数组元素的值6被写入单元rr。
现在,您可以转到间接寻址。 该库具有其自己的数据类型,用于指向RAM存储器空间的指针-MEMPtr 。 让我们看看如何使用它。 我们修改前面的示例,以便通过指针执行内存中变量的工作。
var m = new Mega328(); var bt1 = m.BYTE(); var bt2 = m.BYTE(); var rr = m.REG(); var ptr = m.MEMPTR();
从文本可以看出,我们首先声明了ptr指针 ,然后对其进行了写入和读取操作。 除了能够在执行期间更改命令中的读/写地址之外,使用指针还简化了数组的工作,将读/写操作与指针的增/减组合在一起。 让我们看一个可以用特定值填充数组的程序。
var m = new Mega328(); var bt1 = m.ARRAY(4);
在此示例中,我们利用了在写入内存时增加指针的功能。
接下来,我们继续讨论库控制命令流的能力。 如果更简单,则如何使用该库对有条件的和无条件的跳转和循环进行编程。 管理此问题的最简单方法是使用标签导航命令。 程序中的标签以两种不同的方式声明。 首先是与AVRASM.Label团队合作,我们创建了一个标签以供将来使用,但不要将其插入程序代码中。 此方法用于创建前向跳转,也就是说,在跳转命令必须位于标签之前的情况下。 要将标签设置在汇编代码的必需位置,必须运行命令AVRASM.newLabel([以前创建的标签的变量]) 。 要回退,可以通过使用一个不带参数的命令AVRASM.newLabel()设置标签并将其值分配给变量来使用更简单的语法。
最简单的过渡类型是无条件过渡。 要调用它,我们使用GO([jump_mark]]命令 。 让我们来看一个例子。
var m = new Mega328(); var r = m.REG();
条件转换对执行流程有更多控制。 它们的行为取决于操作标志的状态,这使得可以根据其执行结果来控制操作流程。 该库使用IF函数来描述仅在特定条件下应执行的命令块。 让我们来看一个例子。
var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); rr1.Load(0x22); rr2.Load(0x33); m.IF(rr1 == rr2, () => { AVRASM.Comment(" - , "); }); var t = AVRASM.Text(m);
由于IF命令的语法不是很熟悉,因此请更详细地考虑它。 这里的第一个参数是过渡条件。 以下是放置代码块的方法,如果满足条件,则应执行该方法。 该函数的一种变体是描述替代分支的能力,即,如果不满足条件,则必须执行的代码块。 此外,您可以注意函数AVRASM.Comment() ,我们可以使用该函数向输出汇编器添加注释。
var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); rr1.Load(0x22); rr2.Load(0x33); m.IF(rr1 == rr2, () => { AVRASM.Comment(" - , "); },()=> { AVRASM.Comment(" - , "); }); AVRASM.Comment(" "); var t = AVRASM.Text(m);
在这种情况下的结果将如下所示
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi R0000,34 ldi R0001,51 cp R0000,R0001 brne L0002 ;--- - , --- xjmp L0004 L0002: ;--- - , --- L0004: ;--- --- .DSEG
前面的示例显示了条件分支选项,其中使用比较命令来确定分支条件。 在某些情况下,这不是必需的,因为转换条件应由上一次执行操作后的标志状态确定。 针对此类情况提供了以下语法。
var m = new Mega328(); var rr1 = m.REG(); rr1.Load(0x22); rr1--; m.IFEMPTY(() =>AVRASM.Comment(", 0")); var t = AVRASM.Text(m);
在此示例中, IFEMPTY函数在递增之后检查Z标志的状态,并在条件块达到0时执行条件块的代码。
在使用方面最灵活的可以认为是LOOP功能。 它旨在方便地描述程序循环。 考虑她的签名
LOOP(Register iter, Action<Register, string> Condition, Action<Register, string> body)
iter参数分配一个寄存器变量,该变量可用作循环中的迭代器。 第二个参数包含一个代码块,描述退出循环的条件。 分配的迭代器和要返回的循环的开始标签将传递到此代码块。 最后一个参数用于描述循环主体的代码块。 使用LOOP函数的最简单示例是存根循环,即,跳转到同一行的无限循环。 这种情况下的语法如下
m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { });
编译结果如下。
L0002: xjmp L0002
让我们回到用特定值填充数组并对其进行更改以使填充在循环中执行的示例
var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); var arr = m.ARRAY(16); var ptr = m.MEMPTR(); ptr.Load(arr[0]);
在这种情况下的输出代码如下所示
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi YL, LOW(L0002+0) ldi YH, HIGH(L0002+0) ldi R0001,16 ldi R0000,170 L0003: st Y+,R0000 dec R0001 brne L0003 L0004: .DSEG L0002: .BYTE 16
组织过渡的另一种方法是通过间接寻址的过渡。 在高级语言中,最接近它们的类似物是函数的指针。 在这种情况下,指针将不指向RAM空间,而是指向程序代码。 由于AVR具有哈佛架构,并使用其自己的特定指令集来访问程序存储器,因此ROMPtr用作指针,而不是上述的MEMPtr。 可以通过以下示例说明间接寻址的过渡的用例。
var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var ptr = m.ROMPTR(); ptr.Load(block1);
在此示例中,我们有3个命令块。 完成每个块后,控制权将转移回间接寻址的分支命令。 由于在命令块的末尾每次都将过渡向量设置为一个新块,因此执行看起来像是Block1→Block2→Block3→Block1 ...,依此类推。 该命令与条件分支命令一起,允许该语言使用简单方便的方式来描述诸如状态机之类的相当复杂的算法。
间接寻址分支的更高级版本是SWITCH命令。 它不使用指向过渡标签的指针进行过渡,而是使用指向存储过渡标签地址的内存中变量的指针。
var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var arr = m.ARRAY(6); var ptr = m.MEMPTR();
在此示例中,转换顺序如下:Block1→Block2→Block3→Block1→Block3→Block1→Block3→Block1 ...我们能够实现仅在第一个周期执行Block2命令的算法。
在文章的下一部分中,我们将考虑使用外围设备,实现中断,例程等。