←第1部分。初次认识
第3部分。间接寻址和流控制→
AVR微控制器的汇编代码生成器库
第2部分。入门
按照计划,在这一部分中,我们将更详细地考虑使用NanoRTOS库进行编程的功能。 那些开始阅读本文的人可以熟悉上一篇文章中库的一般描述和功能。 由于计划中的出版物范围有限,因此假定尊敬的读者至少至少对C#编程非常熟悉,并且对Mega系列AVR控制器的汇编语言的体系结构和编程有所了解。
最好将任何技术的研究与示例的执行结合起来,因此,我建议从https://drive.google.com/open?id=1FfBBpxlJkWC027ikYpn6NXbOGp7DS-5B下载该库本身,并尝试将其连接到控制台应用程序项目。 如果成功,则可以安全地继续前进。 您可以将任何C#应用程序或UnitTest项目用作执行示例的外壳。 我个人更喜欢后者,因为它使您可以在一个地方存储几个不同的示例并根据需要执行它们。 无论如何,清单中仅包括示例文本,以节省空间。
使用库时,应始终以诸如微控制器之类的公告开始。 由于参数和外围设备集取决于控制器的类型,因此特定控制器的选择会影响汇编代码的形成。 编写程序的控制器的声明行如下
var m = new Mega328();
此外,可以遵循微控制器的其他设置,例如时钟参数或输出的系统功能分配。 例如,使用硬件重置的权限将使您不必将输出用作端口。 所有控制器参数都有默认值,在示例中我们将省略它们,除非很重要,但在实际项目中,我建议您始终安装它们。 例如,时钟设置可能如下所示
m.FCLK = 16000000; m.CKDIV8 = false;
此设置意味着微控制器由石英谐振器或频率为16 MHz的外部源提供时钟,并且外围设备的分频器关闭。
AVRASM静态类的Text函数负责作品的输出。 该函数将始终在代码末尾被调用,以以汇编器形式输出结果。 该函数将先前分配的控制器类实例分配为参数。 因此,用于库的程序的最简单框架采用以下形式
var m = new Mega328();
如果我们尝试运行该程序,它应该成功,但是不会生成任何代码。 尽管结果毫无用处,但仍然有理由得出结论,该库未生成任何包装代码。
我们已经学习了如何创建一个空程序。 现在,让我们尝试在其中创建一些代码。 让我们从最原始的开始。 让我们看看如何解决位于任意RON单元中的8位变量的增量问题。 从汇编程序的角度来看,这是inc [register]命令。 将以下几行插入我们采购程序的主体中
var r = m.REG(); r++;
团队的目的很明显。 第一条命令将变量r与一个处理器寄存器关联。 第二条命令讨论了增加此变量的必要性。 执行后,我们得到代码执行的第一个结果。
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 inc R0000 .DSEG
让我们仔细看看结果如何。 前四个命令是堆栈指针的初始化。 接下来是变量名称的定义。 最后,我们的inc ,一切由此开始。 除了堆栈的初始化外,没有其他任何东西。 查看此代码时可能出现的问题是哪种R0000 ? 我们有一个名为r的变量。 在C#程序中,程序员可以使用范围很自觉和合法地使用相同的名称。 从汇编程序的角度来看,所有标签和定义都必须是唯一的。 为了不强迫程序员监视名称的唯一性,默认情况下,名称由系统生成。 但是,在某些情况下,出于调试目的,您仍想将有意识的名称从程序传输到输出代码,以便可以轻松找到它。 不吓人。 将m.REG()替换为m.REG(“ r”) ,然后再次运行代码。 结果,我们将看到以下内容
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r20 inc r .DSEG
因此,随着寄存器的命名整理出来。 现在让我们看看为什么突然从20开始分配寄存器,而不是从0开始分配? 为了回答这个问题,我们记得从16开始,寄存器有很大的机会用常量初始化它们。 并且由于对此功能的需求很大,因此我们从上半部分开始分发,以增加优化机会。 然后都一样不清楚-为什么选择c20而不是16? 原因是如果不使用临时存储单元,就不可能将许多命令转换为汇编代码。 为此,我们从16到19分配了4个单元。这并不意味着程序员完全无法访问它们。 只是访问它们的方式有所不同,因此程序员可以意识到它们使用的可能限制并自觉采取行动。 我们从代码中删除寄存器r的定义,并将其后的行替换为
m.TempL ++;
让我们看一下结果
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 inc TempL .DSEG
在这里,显然应该注意,输出汇编程序需要使用开发包中的定义和宏Common.inc正确解释连接文件。 它实际上包含所有必需的宏和定义,包括临时存储单元的名称匹配。 即,TempL = r16,TempH = r17,TempQL = r18,TempQH = r19。 在这种情况下,我们没有使用任何会使用临时存储单元的命令,因此我们在TempL操作中使用它的决定是可以接受的。 如果我们完全确定没有常量对变量的赋值不会发光并且我们不想在上面花费上半部分的宝贵单元格,该怎么办? 通过将其定义更改为var r = m.REGL(“ r”);将我们的定义返回到源代码。 并评估分娩结果
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DSEG
目标已实现。 我们设法向图书馆解释了我们认为该变量应位于的位置。 让我们走得更远。 让我们看看如果一次声明多个变量会发生什么。 我们再复制一次定义和动作。 对于更改,我们将重置一个新变量,并将另一个变量的值减少1。结果应该是这样的。
var m = new Mega328(); var r = m.REGL("r"); r++; var rr = m.REGL("rr"); rr--; var rrr = m.REGL("rrr"); rrr.Clear(); var t = AVRASM.Text(m); . RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DEF rr = r5 dec rr .DEF rrr = r6 clr rrr .DSEG
太好了 这就是所要求的。 现在,让我们看看如果不再需要该寄存器,该如何释放它作其他用途。 不幸的是,到目前为止,这里的一切都是手工完成的。 出国使用代码生成模式时,关于可见性边界和变量自动释放的C#规则尚不起作用。 让我们看看如何在必要时仍然释放单元格。 仅向我们的程序添加一行,然后查看结果。
var m = new Mega328(); var r = m.REGL("r"); r++; var rr = m.REGL("rr"); rr--; r.Dispose(); var rrr = m.REGL("rrr"); rrr.Clear(); var t = AVRASM.Text(m);
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DEF rr = r5 dec rr .UNDEF r .DEF rrr = r4 clr rrr .DSEG
显而易见,我们释放的第四个寄存器再次可用。 考虑到每个新变量声明都捕获了寄存器这一事实,我们可以得出结论,在编译程序时,如果您不希望遇到稀缺的情况,则需要按时释放寄存器。
在分析示例时,我们已经演示了如何在寄存器上执行单播操作。 现在,让我们看看多播的情况如何。 处理器体系结构最多允许两个地址的指令(特别针对腐蚀性指令,但两个指令的结果存放在固定寄存器中)。 应该以这样一种方式来理解,即操作执行后的第一个操作数将包含结果。 此类特殊操作提供了特殊语法[register1] [operation] = [register2] 。 让我们看看它在实际中的外观。 让我们尝试声明并添加两个寄存器变量。
var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1 += op2; 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 add R0000,R0001 .DSEG
得到了他们的期望。 您可以自己尝试操作-,&,|。 并确保结果不会更糟。
到目前为止,以上所有内容显然还不足以编写最简单的程序。 事实是我们还没有涉及寄存器本身的初始化。 微控制器的体系结构允许您使用常数,另一个寄存器值,特定地址的RAM存储单元值,特殊寄存器对中的指针处的RAM存储单元值,特定地址处的输入/输出单元值以及指针处的程序存储单元值来初始化寄存器放在一对特殊的寄存器中。 稍后我们将处理间接寻址,但现在我们将考虑更简单的情况。 我们将编写并执行以下测试程序。
var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1.Load(0x10); op2.Load('s'); op1.Load(op2); var t = AVRASM.Text(m);
在这里,我们用数字和符号声明并初始化了两个变量,然后将变量op2的值复制到单元格op1中。 显然,该数字必须落在0-255的范围内,这样才不会发生错误。 结果将是
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi R0000,16 ldi R0001,'s' mov R0000,R0001 .DSEG
从该示例可以看出,对于所有列出的操作,都使用了一个函数,并且库本身生成了正确的汇编器命令集。 如多次提到的那样,仅使用寄存器的上半部分,可使用ldi命令将数据直接加载到寄存器中。 通过更改程序,使我们的库更加复杂,以便它为下半部分的变量分配寄存器。
var m = new Mega328(); var op1 = m.REGL(); var op2 = m.REGL(); op1.Load(0x10); op2.Load('s'); op1.Load(op2); var t = AVRASM.Text(m);
我们得到
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r4 .DEF R0001 = r5 ldi TempL,16 mov R0000,TempL ldi TempL,'s' mov R0001,TempL mov R0000,R0001 .DSEG
图书馆为应付这项任务,同时花费了最少的团队人数。 同时,我们了解了为什么需要分配临时存储寄存器。 好吧,最后,让我们看看如何使用数学实现常量。 我们知道存在subi assembler命令以从寄存器中减去常数的情况,现在我们将尝试用库的形式对其进行描述。
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 -= 10; var t = AVRASM.Text(m);
结果将是
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 subi R0000,0x0A .DSEG
原来如此。 如果没有可以执行必要操作的汇编器命令,该库将如何运行? 例如,如果我们不想减去,而是添加一个常数。 让我们尝试看看
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 += 10; var t = AVRASM.Text(m);
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 subi R0000,0xF6 .DSEG
该库通过减去负值而退出。 让我们看看这种变化是如何进行的。 将寄存器的值向右移动5。
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 >>= 5; var t = AVRASM.Text(m);
结果将是
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 swap R0000 andi R0000,15 lsr R0000 .DSEG
在这里,并不是所有事情都是显而易见的,但是与使用五个班次的正面解决方案相比,它可以使两个团队的运行速度更快。
因此,在本文中,我们检查了该库用于寄存器算术运算的使用。 在下一篇文章中,我们将继续描述该库如何与指针一起工作,并考虑用于控制命令执行流程(循环,转换等)的方法。