Close

Using macros

A project log for Semyon

A small simon game using the 8 pin STC15F104W, written in 8051 assembly using the SDAS(ASXXXX) assembler from the SDCC toolchain.

hummusprinceHummusPrince 07/31/2020 at 17:340 Comments

Suppose you want to enable or disable external interrupts with certain configurations. You'll have to wiggle some SFR bits for the purpose, probably involving multiple SFRs.

For me, it's looking thus:

;this is ext_int_enable
    orl TCON, #0x05     ;IT1|IT0 - falling edge only
    orl IE, #0x05    ;EX1 | EX0
    orl AUXR2, #0x30    ;EX3 | EX2

;this is ext_int_disable
    anl AUXR2, #~0x30    ;EX3 | EX2
    anl IE, #~0x05    ;EX1 | EX0

This is quite ugly. I want to write it down only once, and than use it couple of times. It makes the code more readable as the purpose of code snippets is made clear to the reader, and reduces the nuisance of writing the whole thing multiple times, reducing bugs and errors introduced by non-careful typeing.

Sure I can treat these as functions, with calls to their label and returns, but it will miss the point - putting aside the overhead of the function call, which can grow quite large for configurations-rich code, this is not the conceptual Idea I wanted to use in the first place.

What I really want is that the assembler will replace each of these tags:

ext_int_enable

with the code snippet:

    orl TCON, #0x05     ;IT1|IT0 - falling edge only
    orl IE, #0x05    ;EX1 | EX0
    orl AUXR2, #0x30    ;EX3 | EX2

I want it to be replaced directly everywhere the code where the tag appears. I want it to simply delete the tag and paste the relevant snippet instead, in the source code.

For those of you familiar with C, this is akin to using preprocessor macros (conceptually you can achieve it with an inline function too, given you forcethe compiler to inline).

Good assemblers, such as ASXXXX which SDAS is based upon, support macros which act just that way. The way to use these with SDAS looks thus:

;ext_int
.macro ext_int_enable
    orl TCON, #0x05     ;IT1|IT0 - falling edge only
    orl IE, #0x05    ;EX1 | EX0
    orl AUXR2, #0x30    ;EX3 | EX2
.endm

.macro ext_int_disable
    anl AUXR2, #~0x30    ;EX3 | EX2
    anl IE, #~0x05    ;EX1 | EX0
.endm

Calling macros is almost trivial. In the last post I defined my external interrupt handler thus:

ext_interrupt_handler:
    anl AUXR2, #~0x30    ;EX3 | EX2
    anl IE, #~0x05        ;EX1 | EX0
    reti

The inside is ext_int_disable, which can simply be called as a macro defined earlier: 

ext_interrupt_handler:
    ext_int_disable
    reti

The assembler replaces the ext_int_disable symbol with the internals of the macro definition above before assembling it. Quite neat IMO.

Macro arguments

Say I want to do something cleverer than static configuration of SFRs, e.g. configuring a timer to some value:

    mov TL0, #(0x10000-count)&0xff
    mov TH0, #((0x10000-count)>>8)&0xff

Where count is the number of timer cycles I want. I might want to use this in several places with different cycle count (that are constant in the code), or rather change this value upon assembly with variable flags (say, different values for different main-clock frequencies).

One must pass the value to the macro with each use, some how. Luckily, ASXXXX is smart enough to do it quite trivially, by adding the arguments with commas to the macro definition:

.macro t0_set_count, count
    mov TL0, #(0x10000-count)&0xff
    mov TH0, #((0x10000-count)>>8)&0xff
.endm

The use is similar. Possible use I had in my code:

delay_debounce:
    t0_set_count, 0x0010
    sjmp delay_activate

delay_display2:
    t0_set_count, 0x2000
    sjmp delay_activate

delay_display:
    t0_set_count, 0x5000
    sjmp delay_activate

 Notice that the values are constant. They can't change on the fly, only during assembly time. To change these values midrun, one must use functions rather than macros. Other possible solution is using a macro with static variables instead of the "count" argument, and change these variables between macro calls.

Advanced macros

Assume one want a macro even more elaborate. For example, I want to choose sleep-mode upon calling some macro. I might want it to look something like this

.macro ext_int_get_input, pd_flag
ext_int_get_input_beginning:
    clear_ext_int_flags     ;Necessary
    ext_int_enable
    interrupt_enable
    .if pd_flag = 1
        orl PCON, #0x01     ;PD = power down
    .else
        orl PCON, #0x01     ;IDL
    .endif
    interrupt_disable
.endm

 First, notice that I call macros from within this macro definition. This is called macro nesting, and is allowed by ASXXXX up to a depth of 20 calls-within-calls. The number is arbitrary, but should be plenty for virtually all users.

The .if and .else lines are assembly-time if/else expressions, akin to #IF/#ELSE in C preprocessor. These will choose whether the assembler will put one branch in the code, or the other.

In the example above, calling

ext_int_get_input, 1

will effectively be replaced with

    clear_ext_int_flags     ;Necessary
    ext_int_enable
    interrupt_enable
    orl PCON, #0x01     ;IDL
    interrupt_disable

so that the .if branch is ignored and not assembled.

This is fine for using only one of the options, as if pd_flag is a global assembly flag. However, using both the options in the same code will raise an error:

<m>   Multiple  definitions  of  the  same label, multiple
            .module directives, multiple conflicting  attributes
            in  an  .area or .bank directive or the use of .hilo
            and lohi within the same assembly.</m>

The wits of ASXXXX macro processor are limited, and different pd_flag values require it to assemble the macro once again, but the label "ext_int_get_input_beginning" is already in use by the other version of the macro, and it simply can't be assembled.

For us it's clear that each instance of the macro is different, and expect the assembler to assign these with different names such as "ext_int_get_input_beginning10000$" and "ext_int_get_input_beginning10001$" depending on the instance, but it really can't make this abstraction for us.

There's a manual way for doing it:

.macro ext_int_get_input, pd_flag, ?rand
ext_int_get_input_beginning'rand: 
    clear_ext_int_flags     ;Necessary.
    ext_int_enable
    interrupt_enable
    .if pd_flag = 1
        orl PCON, #0x01     ;PD = power down
    .else
        orl PCON, #0x01     ;IDL
    .endif
    interrupt_disable
.endm

 The '?' in ?rand is a special letter which means that rand is getting a randomly generated value unless directly assigned a value for. This random value will change each time the macro is called.

the ' operator is for symbol name concatenation. If ?rand gets the value "10097" than "ext_int_get_input_beginning'rand" will be replaced by the label "ext_int_get_input_beginning10097$".

This is a rather nasty business to write down, but it does work very well and allow complex macros to be written.

That's about all there is to tell about macros in ASXXXX.

Discussions