宏的魔力,或如何使AVR汇编程序程序员的工作更轻松

关于汇编器中的宏的文章很多。 以及在文档和各种文章中。 但是在大多数情况下,可以归结为简单列出指令及其功能的简要说明,也可以归结为一组不同的现成宏的示例。
本文的目的是描述一种特定的汇编语言编程方法,以使用宏生成最简单易读的代码。 本文将不会描述单个命令和指令的语法。 制造商已经给出了详细说明。 我们将专注于如何利用这些机会来解决特定问题。


一次,ATMEL尝试并开发了一系列具有高质量架构和简单但功能强大的命令系统的八位微控制器。 但是,如您所知,完美无止境,一些常用的说明还不够。 幸运的是,宏汇编程序由制造商免费提供,可以通过使用指令来大大简化代码。 在直接进入宏之前,我们将执行一些初步步骤


常数的定义


.EQU FOSC = 16000000 .EQU CLK8 = 0 

这两个定义使您可以摆脱宏中的“幻数”,在宏中,寄存器的值是根据处理器频率和外围分频器的保险丝状态来计算的。 第一个定义是处理器晶体的频率,以赫兹为单位,第二个定义是外围分频器的状态。


注册命名


 .DEF TempL = r16 .DEF TempH = r17 .DEF TempQL = r18 .DEF TempQH = r19 .DEF AL = r0 .DEF AH = r1 .DEF AQL = r2 .DEF AQH = r3 

乍一看,可以在宏中使用的命名寄存器有点多余。 如果我们要处理32位值(例如,在两个16位数字相乘的操作中),则仅需要四个Temp寄存器。 如果我们确定两个临时存储寄存器足以供我们在宏中使用,则无法确定TempQLTempQH 。 使用乘法运算的宏需要A的定义。 如果我们不对宏使用32位算术, 不再需要AQ


宏来实现简单的命令


现在我们已经弄清了寄存器的命名,我们将开始实现缺少的命令,并首先尝试简化现有命令。 AVR汇编程序具有一个尴尬的功能。 对于输入和输出,前64个端口使用in / out命令,其余lds / sts使用 。 为了避免每次都在寻找特定端口的必要命令时查看文档,我们将创建一组通用命令,这些命令将独立替换必要的值。


 .MACRO XOUT .IF @0<64 out @0,@1 .ELSE sts @0,@1 .ENDIF .ENDMACRO .MACRO XIN .IF @1<64 in @0,@1 .ELSE lds @0,@1 .ENDIF .ENDMACRO 

为了使替换正常工作,宏中使用了条件编译。 如果端口地址小于64,则执行第一个条件部分,否则执行第二个条件部分。 我们的宏完全重复了用于输入/输出端口的标准命令的功能,因此,为了表明我们的团队具有高级功能,我们添加了标准命名前缀X。
将常量写入输出输入寄存器的命令是汇编器中不可用但经常需要的最常见命令之一。 该命令的宏实现将如下所示


 .MACRO OUTI ldi TempL,@1 .IF @0<64 out @0, TempL .ELSE sts @0, TempL .ENDIF .ENDMACRO 

在这种情况下,为了不违反命令命名逻辑,宏中的名称应在标准名称后添加后缀I ,开发人员使用后缀I表示用于处理常量的命令。 在此宏中,我们使用先前定义的TempL寄存器进行操作
在某些情况下,不需要单个寄存器,而是需要一对完整的寄存器来存储16位值。 创建一个新的宏以将16位值写入一对I / O寄存器


 .MACRO OUTIW ldi TempL,HIGH(@1) .IF @0<64 out @0H, TempL .ELSE sts @0H, TempL .ENDIF ldi TempL,LOW(@1) .IF @0<64 out @0L, TempL .ELSE sts @0L, TempL .ENDIF .ENDMACRO 

在此宏中,我们使用内置的LOWHIGH函数从16位值中提取低字节和高字节。 在宏名称中,将后缀IW添加到命令中,以指示在这种情况下,该命令使用16位值(字)。
程序中经常会加载寄存器对,例如,用于设置指向内存的指针。 让我们创建一个这样的宏


 .MACRO ldiw ldi @0L, LOW(@1) ldi @0H, HIGH(@1) .ENDMACRO 

在此宏中,我们使用了以下事实:制造商的寄存器和端口的标准命名意味着双字节值的较低部分为后缀L ,较高部分为后缀H。 如果在命名自己的变量时遵循此规则,则宏将正确运行,包括它们。 宏的优点还在于它们提供了简单的替换,因此,在第二个操作数是数字的情况下,以及在这是标签名称的情况下,宏将正常工作。


用于实现复杂命令的宏。


当涉及到更复杂的操作时,通常不使用宏,而是使用例程。 但是,在这些情况下,宏可以使生活更轻松,并使代码更具可读性。 在这种情况下,可以进行条件编译。 编程方法可能如下所示:
我们将所有例程放在单独的文件中,我们将其命名为Library.inc 。 该文件中的每个子例程将具有以下格式


 _sub0: .IFDEF __sub0 ; -----    ----- ret .ENDIF 

在这种情况下,__ sub0定义的存在意味着子例程必须包含在结果代码中。 否则,它将被忽略。
接下来,在单独的文件Macro.inc中,我们定义以下形式的宏


 .MACRO SUB0 .IFNDEF __sub0 .DEF __sub0 .ENDIF ; ---          call _sub0 .ENDMACRO 

使用此宏时,我们检查__sub0的定义,如果缺少,则执行确定。 结果,使用宏可解锁输出文件中包含子例程代码。 在宏中使用例程的情况下,主程序的代码将采用以下形式


 .INCLUDE “Macro.inc” ;----    ---- .INCLUDE “Library.inc” 

例如,我们给出了一个宏的实现,该宏用于划分8位无符号整数。 我们保留制造商的逻辑,并将结果放在AL(r0)中 ,除法的其余部分放在AH(r1)中 。 该子例程如下所示


 _div8u: .IFDEF __ div8u ;AH -  ;AL  ;TempL -  ;TempH -  ;TempQL -  clr AL; clr AH; ldi TempQL,9 d8u_1: rol TempL dec TempQL brne d8u_2 ret d8u_2: rol A sub AH, TempH brcc d8u_3 add AH,TempH clc rjmp d8u_1 d8u_3: sec rjmp d8u_1 .ENDIF 

使用此例程的宏定义如下


 .MACRO DIV8U .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 mov TempH, @1 call _div8u .ENDMACRO 

如果需要,可以添加一个用于常量的版本


 .MACRO DIV8UI .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 ldi TempH, @1 call _div8u .ENDMACRO 

结果,在程序文本中使用除法运算很简单


 DIV8U r10, r11 ; r0 = r10/r11 r1 = r10 % r11 DIV8UI r10, 35 ; r0 = r10/35 r1 = r10 % 35 

使用条件编译,我们可以将所有可能对我们有用的例程放在Library.inc中 。 在这种情况下,只有那些至少被调用一次的代码才会出现在输出代码中。 注意入口标签的位置。 标签的输出超出条件的范围是由于编译器的功能。 如果将标签放在条件块的主体中​​,则编译器可能会引发错误。 代码中未使用标签的存在并不令人恐惧,因为任何数量的标签的存在都不会影响结果。


外围宏


不使用制造商的文档很难进行的操作之一是初始化外围设备。 即使使用代码中的寄存器和位的助记符指定,也很难理解设备是在哪种模式下配置的,尤其是因为有时该模式是由不同寄存器的位值组合而成的。 让我们看看如何在USART示例中使用宏。
让我们从异步模式初始化宏开始。


 .MACRO USART_INIT ; speed, bytes, parity, stop-bits .IF CLK8 == 0 .SET DIVIDER = FOSC/16/@0-1 .ELSE .SET DIVIDER = FOSC/128/@0-1 .ENDIF ; Set baud rate to UBRR0 outi UBRR0H, HIGH(DIVIDER) outi UBRR0L, LOW(DIVIDER) ; Enable receiver and transmitter .SET UCSR0B_ = (1<<RXEN0)|(1<<TXEN0) outi UCSR0B, UCSR0B_ .SET UCSR0C_ = 0 .IF @2 == 'E' .SET UCSR0C_ |= (1<<UPM01) .ENDIF .IF @2 == 'O' .SET UCSR0C_ |= (1<<UPM00) .ENDIF .IF @3== 2 .SET UCSR0C_ |= (1<<USBS0) .ENDIF .IF @1== 6 .SET UCSR0C_ |= (1<<UCSZ00) .ENDIF .IF @1== 7 .SET UCSR0C_ |= (1<<UCSZ01) .ENDIF .IF @1== 8 .SET UCSR0C_ = UCSR0C_ |(1<<UCSZ01)|(1<<UCSZ00) .ENDIF .IF @1== 9 .SET UCSR0C_ |= (1<<UCSZ02)|(1<<UCSZ01)|(1<<UCSZ00) .ENDIF ; Set frame format outi UCSR0C,UCSR0C_ .ENDMACRO 

使用宏可以使我们用无法理解的值替换USART设置寄存器的初始化,而无需一行甚至连那些初次接触该控制器的人都可以处理的行来阅读文档。 在这个宏中,最后也很清楚为什么我们确定了频率和除数常数。 好吧,应该指出的是,尽管宏本身的代码令人印象深刻,但是所得到的宏的外观与我们以通常的方式编写初始化的外观相同。
为了完成USART,这里还有一些小宏


  .MACRO USART_SEND_ASYNC outi UDR0, @0 .ENDMACRO 

只有一行,但是使用此宏将使您可以更好地查看程序在USART中显示数据的位置。 如果我们假设不使用中断而在同步模式下工作,那么最好使用下面的宏代替USART_SEND_ASYNC


  .MACRO USART_SEND USART_Transmit: xin TempL, UCSR0A sbrs TempL, UDRE0 rjmp USART_Transmit outi UDR0, @0 .ENDMACRO 

在这种情况下,我们启用端口占用率检查并仅在端口空闲时显示数据。 显然,这种使用外围设备的方法将适用于任何设备,而不仅限于USART


比较不使用宏和使用宏的程序。


让我们看一个小例子,将不使用宏的代码与使用它们的代码进行比较。 例如,选择一个显示经典“ Hello world!”的程序。 通过硬件UART连接到终端。


  RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 USART_Init: out UBRR0H, r17 out UBRR0L, r16 ldi r16, (1<<RXEN0)|(1<<TXEN0) out UCSRnB,r16 ldi r16, (1<<USBS0)|(3<<UCSZ00) out UCSR0C,r16 ldi ZL, LOW(STR<<1) ldi ZH, HIGH(STR<<1) LOOP: lpm r16, Z+ or r16,r16 breq END USART_Transmit: in r17, UCSR0A sbrs r17, UDRE0 rjmp USART_Transmit out UDR0,r16 rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0 

这是相同的程序,但是使用宏编写


 .INCLUDE “macro.inc” .EQU FOSC = 16000000 .EQU CLK8 = 0 RESET: ldiw SP, RAMEND; USART_INIT 19200, 8, "N", 1 ldiw Z, STR<<1 LOOP: lpm TempL, Z+ test TempL breq END USART_SEND TempL rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0 

在此示例中,我们使用了上述宏,这使我们能够显着简化程序代码并使之更易于理解。 两个程序中的二进制代码将完全相同。


结论


使用宏可以大大减少程序的汇编代码,使其更易于理解和可读。 条件编译使您可以创建通用命令和过程库,而无需创建冗余输出代码。 缺点是,在声明数据“转发”时,可以根据高级语言的标准(允许的操作和限制)来指出一个非常适度的问题。 例如,此限制不允许通过宏编写用于jmp / rjmp转换的完整通用命令,并在实现复杂逻辑时显着夸大宏本身的代码。

Source: https://habr.com/ru/post/zh-CN465261/


All Articles