Part 3. Indirect addressing and flow control →
As planned, in this part we will consider in more detail the features of programming using the NanoRTOS library. Those who started reading from this post can familiarize themselves with the general description and capabilities of the library in the previous article . Due to the limited scope of the planned publication, it is assumed that a respected reader is at least minimally familiar with C # programming, and also has an understanding of the architecture and programming in assembly language for Mega series AVR controllers.
The study of any technology is best combined with the execution of examples, so I suggest downloading the library itself from https://drive.google.com/open?id=1FfBBpxlJkWC027ikYpn6NXbOGp7DS-5B and trying to connect it to the console application project. If successful, you can safely move on. You can use any C # application or UnitTest project as a shell for executing examples. I personally like the latter more, because it allows you to store several different examples in one place and execute them as needed. In any case, only the text of the example will be included in the listings to save space.
Work with the library should always begin with an announcement such as a microcontroller. Since the parameters and the set of peripheral devices depend on the type of controller, the choice of a specific controller affects the formation of assembler code. The declaration line of the controller for which the program is written is as follows
var m = new Mega328();
Further, additional settings of the microcontroller may follow, such as clock parameters or the assignment of system functions for outputs. For example, permission to use hardware reset eliminates the use of output as a port. All controller parameters have default values, and in the examples we will omit them, except when it is important, but in real projects I advise you to always install them. For example, a clock setting might look like this
m.FCLK = 16000000; m.CKDIV8 = false;
This setting means that the microcontroller is clocked by a quartz resonator or an external source with a frequency of 16 MHz, and the frequency splitter for peripheral devices is turned off.
The Text function of the AVRASM static class is responsible for the output of the work. This function will always be called at the end of the code to output the result in assembler form. The previously assigned instance of the controller class, the function receives as a parameter. Thus, the simplest framework of the program for working with the library takes the following form
var m = new Mega328(); // // // var t = AVRASM.Text(m); // t
If we try to run the program, it should succeed, but it will not generate any code. Despite the futility of the result, this nevertheless gives reason to conclude that the library does not generate any wrapping code.
We have already learned how to create an empty program. Now let's try to create some code in it. Let's start with the most primitive. Let's see how we can solve the increment problem of an eight-bit variable located in an arbitrary RON cell. From an assembler point of view, this is the inc [register] command. Insert the following lines into the body of our procurement program
var r = m.REG(); r++;
The purpose of the teams is quite obvious. The first command associates the variable r with one of the processor registers. The second command talks about the need to increment this variable. After execution, we get the first result of code execution.
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 inc R0000 .DSEG
Let's take a closer look at what happened as a result. The first four commands are initialization of the stack pointer. Next is the definition of the variable name. And finally, our inc , for which everything was started. Nothing extra, except for the initialization of the stack did not appear. The question that may arise when looking at this code is what kind of R0000 ? We have a variable named r ? In a C # program, a programmer can quite consciously and legally use the same names, using scope. From an assembler point of view, all labels and definitions must be unique. In order not to force the programmer to monitor the uniqueness of names, by default the names are generated by the system. However, there is a situation where, for debugging purposes, you still want to transfer the conscious name from the program to the output code so that it can be easily found. Not scary. Replace m.REG () with m.REG (”r”) and run the code again. As a result, we will see the following
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r20 inc r .DSEG
So, with the naming of the registers sorted out. Now let's see why suddenly registers began to be assigned from 20, and not from 0? In order to answer this question, we recall that starting from 16, registers have a great opportunity to initialize them with constants. And since this feature is in great demand, we start the distribution from the upper half in order to increase optimization opportunities. Then all the same it is not clear - why c20 and not 16? The reason is that the translation of a number of commands into assembler code is impossible without the use of temporary storage cells. For these purposes, we have allocated 4 cells from 16 to 19. This does not mean that they have become completely inaccessible to the programmer. It’s just that access to them is organized a little differently, so that the programmer is aware of the possible limitations of their use and acts consciously. We remove the definition of the register r from the code and replace the line following it with
m.TempL ++;
Let's look at the result
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 inc TempL .DSEG
Here, apparently, it should be noted that the output assembler requires for the correct interpretation of the connection file with definitions and macros Common.inc from the development package. It actually contains all the necessary macros and definitions, including name matching for temporary storage cells. Namely, TempL = r16, TempH = r17, TempQL = r18, TempQH = r19. In this case, we did not use any commands that would use temporary storage cells to work, so our decision to use it in the TempL operation is quite acceptable. And what should we do if we are absolutely sure that no assignment of constants to our variable does not shine and we do not want to spend precious cells of the upper half on it? Return our definition to the source code by changing it to var r = m.REGL ("r"); and evaluate the result of labor
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DSEG
The goal is achieved. We managed to explain to the library where, in our opinion, the variable should be placed. Let's go further. Let's see what happens if several variables are declared at once. We copy our definition and actions a couple more times. For a change, we will nullify one new variable, and decrease the value of the other by 1. The result should be something similar.
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
Wonderful. That is what was requested. Now let's see how we can free the register for other purposes if we no longer need it. Here, unfortunately, so far everything is by hand. C # rules on visibility limits and automatic release of variables when going abroad for the code generation mode do not work yet. Let's see how you can still free the cell if necessary. Add only one line to our program and look at the result.
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
It is easy to see that the fourth register that we freed became available again for use. Taking into account the fact that each new variable declaration captures the register, we can conclude that when compiling the program, you need to free the registers in time if you do not want to encounter a situation when they become scarce.
While analyzing the examples, we have already demonstrated along the way how to perform unicast operations on registers. Now let's see how things are with multicast. The processor architecture allows for a maximum of two-address instructions (for especially corrosive ones, with the exception of two instructions for which the result is placed in fixed registers). This should be understood in such a way that the first operand in the operation after its execution will contain the result. The special syntax [register1] [operation] = [register2] is provided for this type of operation. Let's see how it looks in practice. Let's try to declare and add two register variables.
var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1 += op2; var t = AVRASM.Text(m);
As a result, we will see
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
Got what they expected. You can experiment with the operations yourself -, &, | and make sure the result is no worse.
So far, all of the above is clearly not enough to write even the simplest program. The fact is that we have not yet touched upon the initialization of the registers themselves. The architecture of the microcontroller allows you to initialize the registers with a constant, another register value, RAM memory cell value at a specific address, RAM memory cell value at a pointer located in a special pair of registers, input / output cell value at a specific address, as well as program memory cell value at a pointer placed in a special pair of registers. We will deal with indirect addressing later, but for now we will consider simpler cases. We will write and execute the following test program.
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);
Here we declared and initialized two variables with the number and symbol, and then we copied the value of the variable op2 into cell op1. Obviously, the number must fall within the range of 0-255 so that there is no error. The result will be
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
It can be seen from the example that for all the listed operations one function is used, and the library itself forms the correct set of assembler commands. As has been mentioned many times, direct loading of data into the register with the ldi command is available only for the older half of the registers. We make our library more complex by changing the program so that it allocates registers for variables from the lower half.
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);
Get
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
The library coped with this task, spending at the same time the minimum possible number of teams. And at the same time we saw why we needed to allocate temporary storage registers. Well, finally, let's see how mathematics is implemented for working with constants. We know about the existence of the subi assembler command for subtracting the constant from the register, and now we will try to describe it in terms of the library.
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 -= 10; var t = AVRASM.Text(m);
The result will be
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
Happened. And how will the library behave if there is no assembler command that can perform the necessary operation? For example, if we want not to subtract, but to add a constant. Let's try and see
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
The library got out by subtracting a negative value. Let's see how things are going with the shift. Shift the value of the register by 5 to the right.
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 >>= 5; var t = AVRASM.Text(m);
The result will be
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
Not everything is obvious here, but it runs 2 teams faster than if a frontal solution of five shift teams was used.
So, in this article we examined the use of the library for working with register arithmetic. In the next article, we will continue to describe how the library works with working with pointers and consider methods for controlling the flow of command execution (loops, transitions, etc.)