Assembler code generator library for AVR microcontrollers. Part 3

← Part 2. Getting started
Part 4. Programming peripheral devices and handling interrupts →

Assembler Code Generator Library for AVR Microcontrollers

Part 3. Indirect addressing and flow control

In the previous part, we dwelt in sufficient detail on working with 8-bit register variables. If you missed the previous post, I advise you to read it. In it there, you can find a link to the library to try out the examples in the article yourself. For those who downloaded the library earlier, I recommend downloading the latest version, since the library is constantly updated and some examples may not work in the old version of the library.

Unfortunately, the bit depths of the previously considered register variables are clearly not enough to be used as memory pointers. Therefore, before proceeding directly to the discussion of pointers, we consider another class of data description. Most commands in the AVR Mega architecture are designed to work only with register operands, that is, both operands and the result are 8 bits in size. However, there are a number of operations where two consecutively located RON registers are considered as a single 16-bit register. There are few such operations, and they are mainly oriented to work with pointers.

From the point of view of the syntax of the library, working with a register pair is almost the same as working with a register variable. Consider a small example where we try to work with a register pair. In order to save space here and below, we will give the result of execution only where it is necessary to explain certain features of code generation.

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); 

In this example, we declared two 2-byte variables located in register pairs using the DREG () command. With the following commands, we assigned them the initial value and performed a series of arithmetic operations. The example shows that the syntax for working with a register pair is largely the same as working with a regular register. A register pair can also be considered as a variable consisting of two independent registers. The register is accessed as a set of two 8-bit registers through the High property for accessing the upper 8 bits as an 8-bit register, and the Low property for accessing the lower 8 bits. The code will look like this

 var m = new Mega328(); var dr1 = m.DREG(); dr1.Load(0xAA55); dr1.Low--; dr1.High += dr1.Low; var t = AVRASM.Text(m); 

As you can see from the example, we can work with High and Low as independent register variables, including performing various arithmetic and logical operations between them.

Now that we’ve figured out double-length variables, we can begin to describe how to work with variables in memory. The library allows you to work with 8, 16-bit variables and arrays of bytes of arbitrary length. Consider the example of allocating space for variables in RAM.

 var m = new Mega328(); var bt = m.BYTE(); //8-    var wd = m.WORD(); //16-    var arr = m.ARRAY(16); //  16  var t = AVRASM.Text(m); 

Let's see what happened.

 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 

In the data definition section, we have a memory allocation. Note that the allocation order is different from the declaration of variables. This is no coincidence. The allocation of memory for variables occurs after sorting in descending order according to the following criteria (in decreasing order of importance) The maximum divisor multiple of degree 2 → The size of the allocated memory. This means that if we want to allocate 4 arrays of 64, 48,40 and 16 bytes in size, then the allocation order, regardless of the declaration order, will look like this:

Length 64 - Maximum Divisor Multiples of Degree 2 = 64
Length 48 - Maximum Divisor Multiples of Degree 2 = 16
Length 16 - Maximum divisor multiple of degree 2 = 16
Length 40 - Maximum Divisor Multiples of Degree 2 = 8
This is done in order to simplify the control of array boundaries.
and reduce the size of the code in operations with pointers. We cannot directly perform any operations with variables in memory, therefore all that is available to us is reading / writing to register variables. The simplest way to work with variables in memory is direct addressing.

 var m = new Mega328(); var bt = m.BYTE(); // 8-    var rr = m.REG(); //    rr.Load(0xAA); //  rr   0xAA rr.Mstore(bt); //     rr.Clear(); //  rr.Mload(bt); //    var t = AVRASM.Text(m); 

In this example, we declared a variable in memory and a register variable. After that, we assigned the value 0x55 to the variable and wrote it into the variable in memory. Then erased and restored back.

To work with array elements, we use the following syntax

 var rr = m.REG(); var arr = m.ARRAY(10); rr.MLoad(arr[5]); 

The numbering of the elements in the array begins with 0. Thus, in the above example, the value 6 of the array element is written to the cell rr.

Now you can go to indirect addressing. The library has its own data type, MEMPtr, for a pointer to RAM memory space. Let's see how we can use it. We modify our previous example so that the work with the variable in memory is carried out through the pointer.

 var m = new Mega328(); var bt1 = m.BYTE(); var bt2 = m.BYTE(); var rr = m.REG(); var ptr = m.MEMPTR(); //  ptr ptr.Load(bt1); //ptr   bt1 rr.Load(0xAA); // rr - 0xAA ptr.MStore(rr); //  bt1 0xAA rr.Load(0x55); // rr - 0x55 ptr.Load(bt2); //ptr   bt2 ptr.MStore(rr); //  bt2 0x55 ptr.Load(bt1); //ptr   bt1 ptr.MLoad(rr); //  rr  0xAA var t = AVRASM.Text(m); 

It can be seen from the text that we first declared the ptr pointer , and then performed write and read operations with it. In addition to the ability to change the read / write address in the command during execution, the use of the pointer simplifies working with arrays, combining the read / write operation with the increment / decrement of the pointer. Let's look at a program that can fill an array with a specific value.

 var m = new Mega328(); var bt1 = m.ARRAY(4); //   4  var rr = m.REG(); var ptr = m.MEMPTR(); ptr.Load(bt1.Label); //ptr   bt1 rr.Load(0xAA); // rr - 0xAA ptr.MStoreInc(rr); //  bt1 0xAA ptr.MStoreInc(rr); //  bt1+1 0xAA ptr.MStoreInc(rr); //  bt1+2 0xAA ptr.MStoreInc(rr); //  bt1+3 0xAA rr.Clear(); rr.MLoad(bt1[2]); //  rr 3-   var t = AVRASM.Text(m); 

In this example, we took advantage of the ability to increment a pointer when writing to memory.
Next, we move on to the library's ability to control the flow of commands. If it’s easier, how to program conditional and unconditional jumps and loops using the library. The easiest way to manage this is to use label navigation commands. Labels in a program are declared in two different ways. The first is that with the AVRASM.Label team we create a label for future use, but do not insert it into the program code. This method is used to create forward jumps, that is, in cases where the jump command must precede the label. To set the label in the required place of the assembler code, you must run the command AVRASM.newLabel ([variable of the previously created label]) . To go back, you can use a simpler syntax by setting a label and assigning its value to a variable with one command AVRASM.newLabel () without parameters.

The simplest type of transition is an unconditional transition. To call it, we use the GO command ([jump_mark]] . Let's see how this looks with an example.

 var m = new Mega328(); var r = m.REG(); //  var lbl1 = AVRASM.Label;//        m.GO(lbl1); r++; //    r++; AVRASM.NewLabel(lbl1);//  //  var lbl2 = AVRASM.NewLabel();//    r--; //    r--; m.GO(lbl2); var t = AVRASM.Text(m); 

Conditional transitions have more control over the flow of execution. Their behavior depends on the status of the flags of operations and this makes it possible to control the flow of operations depending on the result of their execution. In the library, the IF function is used to describe a block of commands that should be executed only under certain conditions. Let's look at an example.

 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); 

Since the syntax of the IF command is not quite familiar, consider it in more detail. The first argument here is the transition condition. The following is the method in which the code block is placed, which should be executed if the condition is met. A variant of the function is the ability to describe an alternative branch, i.e., a block of code that must be executed if the condition is not met. Additionally, you can pay attention to the function AVRASM.Comment () , with which we can add comments to the output assembler.

 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); 

The result in this case will look as follows

 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 

The preceding examples show a conditional branching option in which a comparison command is used to determine the branching conditions. In some cases, this is not required, since the transition conditions must be determined by the state of the flags after the last operation performed. The following syntax is provided for such cases.

 var m = new Mega328(); var rr1 = m.REG(); rr1.Load(0x22); rr1--; m.IFEMPTY(() =>AVRASM.Comment(",    0")); var t = AVRASM.Text(m); 

In this example, the IFEMPTY function checks the status of the Z flag after the increment and executes the code of the conditional block when it reaches 0.
The most flexible in terms of use can be considered the LOOP function. It is designed to conveniently describe program cycles. Consider her signature

 LOOP(Register iter, Action<Register, string> Condition, Action<Register, string> body) 

The iter parameter assigns a register variable that can be used in the loop as an iterator. The second parameter contains a code block that describes the conditions for exiting the loop. The assigned iterator and the start label of the loop to return are passed to this code block. The last parameter is used to describe the code block of the main body of the loop. The simplest example of using the LOOP function is a stub loop, that is, an infinite loop to jump to the same line. The syntax in this case will be as follows

 m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { }); 

The compilation result is given below.

 L0002: xjmp L0002 

Let's go back to our example of filling an array with a certain value and change it so that filling is done in a loop

 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]); //     rr2.Load(16); //    rr1.Load(0xAA); //   m.LOOP(rr2, (r, l) => //rr2     . { r--; //   m.IFNOTEMPTY(l); // ,   }, (r,l) => ptr.MStoreInc(rr1)); //   var t = AVRASM.Text(m); 

The output code in this case will look as follows

 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 

Another way to organize transitions is through indirectly addressed transitions. The closest analogue in high-level languages ​​for them is a pointer to a function. The pointer in this case will not point to the RAM space, but to the program code. Since AVR has a Harvard architecture and uses its own specific set of commands to access program memory, ROMPtr is used as a pointer, not MEMPtr described above. The use case for indirectly addressed transitions can be illustrated by the following example.

 var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var ptr = m.ROMPTR(); ptr.Load(block1); //     var loop = AVRASM.NewLabel(); AVRASM.Comment("   "); m.GOIndirect(ptr); //   ,     AVRASM.NewLabel(block1); AVRASM.Comment("  1"); ptr.Load(block2); m.GO(loop); AVRASM.NewLabel(block2); AVRASM.Comment("  2"); ptr.Load(block3); m.GO(loop); AVRASM.NewLabel(block3); AVRASM.Comment("  3"); ptr.Load(block1); m.GO(loop); var t = AVRASM.Text(m); 

In this example, we have 3 blocks of commands. At the end of each block, control is transferred back to the indirectly addressed branch command. Since at the end of the command block we set the transition vector to a new block each time, the execution will look like Block1 → Block2 → Block3 → Block1 ... and so on in a circle. This command, together with conditional branch commands, allows simple and convenient means of the language to describe such fairly complex algorithms as a state machine.

A more sophisticated version of an indirectly addressed branch is the SWITCH command. It does not use a pointer to a transition label for the transition, but a pointer to a variable in the memory in which the address of the transition label is stored.

 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(); //    m.Temp.Load(block1); m.Temp.Store(arr[0]); m.Temp.Load(block2); m.Temp.Store(arr[2]); m.Temp.Load(block3); m.Temp.Store(arr[4]); ptr.Load(arr[0]); //     var loop = AVRASM.NewLabel(); m.SWITCH(ptr); //   ,     AVRASM.NewLabel(block1); AVRASM.Comment("  1"); ptr.Load(arr[2]); //       m.GO(loop); AVRASM.NewLabel(block2); AVRASM.Comment("  2"); m.Temp.Load(block3); ptr.MStore(m.Temp); //       m.GO(loop); AVRASM.NewLabel(block3); AVRASM.Comment("  3"); ptr.Load(arr[0]); //       m.GO(loop); 

In this example, the transition sequence will be as follows: Block1 → Block2 → Block3 → Block1 → Block3 → Block1 → Block3 → Block1 ... We were able to implement an algorithm in which the Block2 commands are executed only in the first cycle.

In the next part of the post, we will consider working with peripheral devices, implementing interrupts, routines, and much more.


All Articles