Path: Home => AVR-EN => Assembler introduction => Comparision hi-lo    (Diese Seite in Deutsch: Flag DE) Logo

Beginner's introduction to AVR assembler language

Comparing high-level language programs with assembler language

The basic question of starters in the microcontroller world, whether to start learning C or asm, is taken away from him nowadays: the Arduino knows only C, and has erected lots of hurdles against assembler. So, he starts with C. And if you had your first lessons in C, what should you bring to the idea to learn an additional language? Now, simply: it extends your horizon, and it's fun.

So, when you start with assembler, you already know other languages. And those stand in the way and hinder you in learning. A lot of times the question arises: why is that so different in assembler than in my well-trained previous language?

For those, that encounter this question lots of times, this page has been written. It demonstrates, using a simple example, from what those differences result, and also demonstrates, where those differences are small. This should simplify learning, and to simply jump over those high hurdles.

As high-level language I use Pascal for the example, just because I know this language best. Other high-level languages, such as C, Java, VBA or PHP, work similarly, just exchange the begin/end by { and }. Also, the differences between those and assembler are mainly the same.

First of all: even though I know Pascal very good, I would never use a Pascal compiler for producing code for an AVR. I use Pascal only on my PC and my laptop. What would be gained if I use Pascal for AVRs? Nothing at all. The very special properties of all AVRs cannot be represented by a Pascal compiler, and I hate compilers that hide half of the available internal hardware from me (Good bye, Bascom!). And I like this freedom to use all this available hardware. So the Pascal I use here is for my 386-laptop, not for an AVR.

One of the basic differences between high-level languages and assembler concerns the use of libraries. I consider people that use libraries extensively are not programmers: they just stack together software that others wrote, use this and adjust it to their own needs. I call that "plugging" instead of "programming": nothing really new is created here. So I will not go any deeper into plugging in this article.

The example: a clock with a seconds ticker

The example that I use here: a clock with seconds, minutes and hours shall start with a certain time and shall be increased each second. The software shall be tested (as any good software), by ticking a whole day, until it reaches again zero. The time shall be formatted as this: "01:23:45".

First, I'll show how this is solved in Pascal on a laptop or PC. Then we solve that same in assembler on an AVR. And you'll learn where exactly the differences between those two solutions are.

1 The task in Pascal

1.1 Variable definitions in Pascal

The variables for the time can be defined with the following source line in Pascal:

Var bSec,bMin,bHr: Byte;

Note that in many high-level languages these kind of definitions are required, for which each language uses different terms, such as Var, Dim or others. Some do not require that, and use their own guess of what type these three variables are.

So far, so simple. What is the Pascal compiler now doing with that? He fixes, somewhere in his large storage space, three bytes for those variables. Not exactly: he just fixes three names for those spaces, by which the programmer will later use those names for calculating and manipulating those three variables. When compiling this line, nothing happens with those variables: the storage space they address still has the same garbage in it, that had been there before, nothing has been done with this storage space yet.

Physically nothing has happened: the space is garbage, no executable code has been produced. The Var line is eaten by the compiler, and nothing happens with that.

One additional thing than can be learned here: I named those three variables starting with a small b: That name always reminds me of the type, that those variables have, a Byte. I always use this reminder, and I am constantly using that. Be it high-level language or assembler: I'll stick to that rule.

1.2 Setting the clock in Pascal

But now it starts: we write to the clock variables and set the time.

In Pascal this is performed by a procedure. This could look like the following code:

Procedure ClockSet(Hour,Minute,Second:Byte);
begin
bSec:=Second;
bMin:=Minute;
bHr:=Hour;
end;

Later on we can call this procedure with the following code:

{ Clears the clock }
Procedure ClockClear;
begin
ClockSet(0,0,0);
end;

{ Set the clock to a fixed starting time }
Procedure ClockFixed;
begin
ClockSet(1,23,44);
end;

The first procedure restarts the clock at 00:00:00, the second starts it at 01:23:44. Now the variables are filled with meaningful content.

At least: the compiler now has produced some code to be executed.

1.3 Clock tick by one second in Pascal

Now, if we want to increase the time by one second, we need the procedure "ClockTick". As it might be interesting to the calling procedure, if a day is over, this procedure needs to report that back. So we do not select a procedure for that, rather we use a function. Functions return a result to the calling procedure, in this case a boolean value that gets true only if the day is over and a new day starts.

Function ClockTick:Boolean;
begin
Result:=False;
Inc(bSec);
If bSec >= 60 Then
  begin
  bSec:=0;
  Inc(bMin);
  If bMin >= 60 Then
    begin
	bMin:=0;
	Inc(bHr);
	If bHr >= 24 Then
	  begin
	  bHr:=0;
	  Result:=True;
	  end;
	end;
  end;
end;

The returned result of this function enables the calling procedure to react to this, e. g. by increasing date variables. But that is beyond our example's scope.

1.4 Testing the clock in Pascal

To be sure that the clock works exact, we'll have to clear the clock and we'll have to tick it 60*60*24 = 86,400 times, and we'll have to check if zero has been reached again. To do this, we'll call the function ClockTick and count how often this returns False. In Pascal we would do this with the following code:

Procedure ClockTest;
Var n:LongInt;
begin
ClockClear;
n:=0;
Repeat Inc(n) Until ClockTick;
Writeln(n);
end;
What will the compiler do here? First he will define a multi-byte variable with the name "n". The multi-byte type is necessary because the 86,400 does neither fit into one 8-bit nor into one 16-bit variable. So LongInt provides enough space for this large number. If our count would have to count the number of seconds in a year or in 100 years, n could be of the type INT64, the largest integer in Pascal.

And now we are at a similar problem in assembler: The AVRs can only handle 8-bit variables in their 32 registers or 16-bit variables (in their four double-registers R25:R24, R27:R26, R29:R28 and R31:R30). But, as he has 32 registers, we can increase the size to up to 256 bits, just by treating all the necessary bytes as one number. And: there is no need to tell the assembler how large the number will be in assembler, that will be done by the code applied on all those registers. So in assembler we can make an INT64, an INT96, or of whatever length will be needed. Not so in Pascal!

Again: the Pascal compiler cares about the variable "n". Where he will place these bytes for n, is his secret. He will place it somewhere on his large stack, we do not know where.

With the Repeat we'll count the seconds, until the day ends. The 86,400 seconds will be shown on one line of the command window, where our executable will run.

1.5 Converting the time to a string in Pascal

If we need to display the current clock, we have to convert the three variables to include the leading zeroes and to add the two : characters. As the conversion of the byte variable to a string has to be applied three times, we write a function for that.

Function ByteToStr(b:Byte):String;
begin
If b < 10 Then
  Result:='0'+IntToStr(b) else
  Result:=IntToStr(b);
end;

Function TimeToStr:String;
begin
Result:='Time = '+ByteToStr(bHr)+':'+ByteToStr(bMin)+':'+ByteToStr(bSec);
end;

If we now call a "Writeln(TimeToStr);" somewhere in the code, we'll get the formatted time as hh:mm:ss on our command line's screen.

Now we have all things together, what we need to set the time to a desired value, to tick this every second, to test the tick routine and to display the clock in a convenient form. And all this in the high-level language Pascal.

2 The same in Assembler

For all those operations we now construct the clock simularly in AVR assembler language. Expect some strange things when we do this.

2.1 Two destinations for the variables in Assembler

The first question, that the Pascal programmer never had to care about: where to place those variables? How to tell the assembler where to place it.

Two places are possible:
  1. in three of its 32 registers, or
  2. in his large static RAM, also called SRAM.
Now this is an enormous difference to Pascal: the programmer is responsible to tell the assembler where to place those. As there are two different places for that, he has to decide that. The assembler has no automatic placement routine, all has to be told him by the source code.

And this forces the programmer to decide: if he has plenty of registers, he will select opportunity 1. If not, he will choose 2. If he started with 1, and if his code later increases and he needs more registers for other stuff, he will have to move to 2. As the time is only changing once per second, the three variables can be placed into the SRAM. As they are to be accessed in comparably rare cases, there is no reason why we cannot accept the additional instructions, that an access to SRAM will require.

What we already learn from this, is that leaving the decision over variable placement with the programmer gives him the opportunity to optimize. Letting the high-level language decide, takes this opportunity away. That is why assembler language programmers hate high-level languages: they take away too much from its own control.

First the formulation to place those in the registers:

.def rSec = R17
.def rMin = R18
.def rHr = R19
That does exactly the same now like the Var line in Pascal: it defines three names (rSec, rmin and rHr) and gives those an address. Here the addresses are pointing to three registers (R17, R18 and R19).

But: why not R0, R1 and R2, at the top of the register park, which AVRs have (not the ATtiny4/5/9/10)? Now, this is implicit knowledge of an AVR assembler programmer: you cannot use the instructions "LDI" and "CPI" on registers below R16. And the LoaD Immediate (LDI) and ComPare Immediate (CPI) are useful in programming the clock. If we would place the three variables to R0, R1 and R2 we'd need two instructions for LDI- and CPI-ing those. And the assembler programmer hates extra instructions: he is a native optimizer, always seeking to avoid unnecessary instructions.

Now this is the real difference between high-level and assembler: high-level-languagers aren't even aware that the compiler has decided about this, while assembler programmers are well aware of the consequences of this decision, and have to decide it on their own.

Like in the Var line in Pascal, the .def are only messages for the assembling: they produce no executable code, but decide only on the place. But don't expect that the assembler will protect this place! You can even define another name that points to the same register. All you get is a warning. And if your code uses R17 instead of bSec to write something different to that place, you don't even get a warning. Assembler, and the Central Processing Unit (CPU) does not know something about protection: they allow any access to anything at any time. Protection only exists in the programmer's mind. A really grown-up language that treats the programmer as grown-up.

If the three variables are to be stored in the SRAM, the def's would look like this:

.dseg
.org SRAM_START
sSec:
.byte 1
sMin:
.byte 1
sHr:
.byte 1
Again, the "s" as first character of the name, is my rule, you can use whatever you want.

These three labels (sSec, smin and sHr) are now associated with addresses: the .byte directives are only making a difference between those three addresses. If you assemble this source code with gavrasm using the option -s (or rather with avr_sim, where this option is automatically selected), you can see those three addresses in the symbol table on the end of the listing (other assemblers do not show you this, they treat it as internal secret - just to add some complication to your life):

List of symbols:
Type nDef nUsed             Decimalval           Hexval Name
  T     1     1                     19               13 ATTINY13
  L     1     0                     96               60 SSEC
  L     1     0                     97               61 SMIN
  L     1     0                     98               62 SHR

Now you know where those three variables are placed: in SRAM with the labels (type L) at the addresses 96, 97 und 98. If you would have assembled it for a very large ATmega device, these addresses might start with 256 instead.

Like with the .def lines, nothing happens with those storage cells in the .dseg. No values are written there, if you inspect their content do not be surprised if you find lots of binary ones therein. While registers are all cleared during the controller's start-up, the SRAM is not cleared.

Again: it is the programmer's responsibility to determine, where something is placed and why this place has been selected for that. The assembler decides nothing by itself.

To place the three variables to the SRAM has more than double the lines than placing them to registers. Nothing happens, but three names (symbols) have been fixed.

2.2 Clearing the clock in Assembler

Not only the definition procedure for the two places is different but also if we want the clock to start at 00:00:00. First the code for the registers:

; Restart the clock
ClockStartReg:
  clr rSec
  clr rMin
  clr rHr
  ret
But even this simple operation can be done in another way, which results in the same:

; Restart the clock
ClockStartReg:
  ldi rSec,0
  ldi rMin,0
  ldi rHr,0
  ret
Do what you want, it is (nearly) all the same. (For experts: of course it is not the same, the second formulation does not clear the C flag in the status register SREG and does not set the Z flag therein.)

Now we can call the "ClockStartReg" subroutine by formulating the following instruction: "rcall ClockStartReg". But that doesn't work correct. The rcall needs a functioning stack. Unlike to high-level languages, assembler does not set up a stack automatically (remember: the programmer is a grown-up and knows when and how to initiate the stack). That requires the following code lines:

Start:
  ; Init the stack
  .ifdef SPH
    ldi R16,High(RAMEND)
	out SPH,R16
	.endif
  ldi R16,Low(RAMEND)
  out SPL,R16
This sets the 8-bit stack pointer in SPL, and, if necessary, the 16-bit stack pointer SPH:SPL to the end of the SRAM. If an rcall is executed, he throws (pushes) the current program address (PC) to the stack in SRAM, and if a RET is executed, pops the stored address back to the PC. All things have to be made and cared of by the programmer: he only knows if he needs the stack or not. Not so the high-level programmer: the compiler, in any case, adds a stack, even if not really needed. As compilers usually use the stack for saving registers, even if the free space is not really needed, they always need it - for nothing at all and for a waste of time and instructions.

One advantage has the assembler programmer: he can place the subroutine for clearing the clock to wherever he wants. Before or after the routine is called, the assembler accepts that all. This is a clear difference to high-level languages, that require routines (procedures, functions) to be defined before they can be called. Otherwise those have to be declared FORWARD. Or you run into one of those nice error messages, as thrown by the compiler at the programmer. Assembler doesn't need this kind of forwarding, they read the source code twice (Two-pass-assembler).

Here the code for starting the clock, if your variables are in SRAM:

ClockStartSram:
  ldi R16,0
  sts sSec,R16
  sts sMin,R16
  sts sHr,R16
  ret
Not very different from the registers, but uses the STS instruction (STore in Sram) with the address and a register, here R16.

2.3 Setting the clock in Assembler

A little bit more interesting is to set the clock to a starting time. The most simple way is the variables in registers:

; Clock in registers, set to 01:23:44
ClockSetReg:
  ldi rSec,44
  ldi rMin,23
  ldi rHr,1
  ret
When having the variables in SRAM the following code sets it:

; Clock in SRAM, set to 01:23:44
ClockSetSram:
  ldi R16,44
  sts sSec,R16
  ldi R16,23
  sts sMin,R16
  ldi R16,1
  sts sHr,R16
  ret
Now, what about if the clock shall start with 20:00:01 tomorrow, so you don't have to wait until the day changes? Now, the assembler programmer now searches for all cases in his code where rSec/sSec, rMin/sMin or rHr/sHr is used. As the source code uses these very often, he has to inspect lots of places, where he would to change these numbers. As most of the cases have nothing to do with setting the clock, a not very promising approach.

To avoid this, we place the following lines on top of the assembler source code:

.equ cClockStartHr = 20
.equ cClockStartMin = 0
.equ cClockStartSec = 1
Instead of "ldi rSec,1" he now writes "ldi rSec,cClockStartSec", and he has to only change the three lines on top.

The same would be done in Pascal with the lines

Const
  cClockStartHr = 20;
  cClockStartMin = 0;
  cClockStartSec = 1;
and by calling "ClockSet(cClockStartHr,cClockStartMin,cClockStartSec)" the same would be achieved.

As you see: just a few words change (Const instead of .equ), but all is the same. Just place those constants on top of your source code, so it is easier to find. In Assembler you can even place the constants at the end of your source code, in most cases this is fine. Do not try that in Pascal, unless you add a FORWARD line on top for it.

And now: where are the calling parameters for "ClockSet", which are handed over in Pascal? Now we are on a real difference between Pascal and assembler: assembler doesn't know any calling parameters. Assembler has an even more powerful mechanism: anything is PUBLIC and GLOBAL at any time and in any case. You can select any place in the whole address world of an AVR to hand over some value. The subroutine that is called gets this value from that location. So, if your value is in register, the subroutine uses this register to read that value. No matter where you put it, the sub can get it from there. Very transparent, no hidden values are handed over from somewhere, as this is the case with Pascal.

What the compiler has to do to copy a calling parameter to a procedure or function: nothing similar has to be performed in assembler. Nothing changes places, and nothing to waste time for. Anything immediately at hand.

Nothing at all is protected in assembler, so the words PUBLIC and GLOBAL are unnecessary here. The other side of the coin: the programmer has to avoid any changes to variables that are not to be changed by other code parts. If your Spaghetti code changes rSec by writing something like 0xFF to it: the assembler will neither complain nor will it protect rSec in any way against those kind of horrible attacks. Anything is possible, even nonsense is. It's all up to you.

Transferring parameters makes no sense in assembler, because anything is freely available at any time. The same applies for the results of functions: the called subroutine can return back anything in any place, so assembler doesn't limit the returned result in any way. This is demonstrated closer in the next example.

2.3 The tick-routine in Assembler

The clock has to be advanced once in each second. No matter how you do that (adding a timer in Pascal or a timer in assembler), it needs this code. Here in assembler:

Tick:
  inc rSec
  cpi rSec,60
  brcs TickRet ; Branch if smaller than 60 
  clr rSec
  inc rMin
  cpi rMin,60
  brcs TickRet ; Branch if smaller than 60
  clr rMin
  inc rHr
  cpi rHr,24
  brcs TickRet ; Branch if smaller than 24
  clr rHr
TickRet:
  ret
The "inc" increases the register's content by one, the "cpi" compares the register content with 60 or 24, and "brcs" branches, if the Carry-flag in the CPU's status register (bit 1 of SREG) is one (BRanch on Carry Set). If 60 seconds are reached, the seconds are cleared and the minutes are increased. And so on, until the day is over. Not very different from the Pascal code, we just do the branching with the carry flag, while the Pascal code hides this jump.

And where is the "Result" of that function? Now, it has even two results. Both are located in the status register of the CPU:
  1. the Z-flag: this flag is set by addition, subtraction and compare instructions. The flag is set if the result of the operation is zero, otherwise it is cleared. As in our case the CPI has not reached zero, as long as the comparison is with numbers in registers that are smaller than that constant, the zero flag is not set in those cases. As the final "ret", after branching, doesn't affect this flag, the cleared zero flag is returned, if the day is not over yet. Only if the comparison of the hours with 24 reaches zero (a comparison is a subtraction with skipping the result setting), the Z flag is set. As the instruction CLR also sets the Z flag, the return with the Z flag set signals "the day is over".
  2. the C-flag: this flag is set if the comparision with the constant was made with a register content that was smaller. The code uses the flag to decide whether the routine is left earlier. So the C flag is always set when the day is not over. Only the comparison of rHr with 24 leaves the carry cleared if 24 has been reached. As the following CLR rHr also clears the C flag, this is the only case where the routine returns with a cleared carry flag.
The Z and the C flag are therefore inverted flags: both can be used to decide, whether a date increase is necessary or not. As the status register is PUBLIC and GLOBAL, this result can be used outside of the tick function code, but only if no other instructions change those flags.

The same principle: when returning from a subroutine, this can hand-over anything at any place. Here the result comes back in the status register, but you can use anything to return anything. Not only one result, like in Pascal's functions, but anything that you like to know outside.

Conclusion: forget the concept with transferring parameters to a procedure or function, and forget the concept to return parameters from functions in assembler: nothing like this makes any sense. Enjoy the freedom to place anything anywhere, that you might need later on, you are the master for all these decisions. But remind yourself that you are responsible for any protection that might be necessary, as well.

An example for this responsibility: assume that your clock tick is inside an interrupt service routine, that receives its tick from a timer once in every second. Now, assume that you want to set the seconds in a routine to 59 (and minutes and hours to a desired time, either). Now assume that the running timer reaches a full second just after you executed the seconds setting, but before the minutes and hours setting. The timer interrupt will now clear the seconds and will increase minutes and/or hours accordingly. After the interrupt service routine has finished its ticking, you'll have the minute and hours overwritten. Now, your clock is wrong by one minute: the seconds are already zero, but the minutes and hours refer to one minute before. It is this kind of things that can happen, if you are in assembler and you use two different sources that access one location. You'll have the responsibility to not allow the timer to tick while you are changing these three bytes. It is simple to achieve, but you have to realize that those two additional instructions are required to protect your clock from this rare case of occurrence and malfuntion. It is only a false time in this case, but if your clock ignites a piece of dynamite, the missing minute might cause people to die from that kind of error. Timers in Pascal simply cannot produce this kind of malfunction, so there is no need to think about those kind of scenarios outside the assembler world.

2.5 The test routine in Assembler

The test routine, that counts the number of seconds until the day changes, "ClockTest", a similar one like in Pascal, is a little bit longer in assembler, because we have to care about the stack and because we have to determine each time, when we touch the counter variable, where this is and, as assembler knows bytes only, how we increase a 24-bit counter.

The test routine counts, after how many calls a day is over. In assembler, with the time variables in registers, this goes like this:

.def rCnt0 = R0 ; 24-bit counter, byte 0
.def rCnt1 = R1 ; dto., byte 1
.def rCnt2 = R2 ; dto., byte 2
.def rmp = R16 ; Multi purpose register
.def rSec = R17 ; Seconds
.def rMin = R18 ; Minutes
.def rHr = R19 ; Hours
;
Start:
.ifdef SPH
  ldi rmp,High(RAMEND)
  out SPH,rmp
  .endif
  ldi rmp,Low(RAMEND)
  out SPL,rmp
; Clock to zero
ClockToStart:
  rcall ClockStart
  clr rCnt0 ; 24-bit-counter to zero
  clr rCnt1
  clr rCnt2
Loop:
  inc rCnt0 ; Count the next tick, byte 0
  brne Tick ; No overflow
  inc rCnt1 ; dto., byte 1
  brne Tick ; No overflow
  inc rCnt2 ; dto., byte 3
Tick:
  rcall ClockTick ; One second up
  brcs Loop ; Loop on carry set
  nop ; Place a breakpoint here
EndLoop:
  rjmp EndLoop
;
; Subroutines
; Start the clock
ClockStart:
  clr rSec
  clr rMin
  clr rHr
  ret
; One-second clock tick
ClockTick:
  inc rSec
  cpi rSec,60
  brcs ClockTickEnd
  clr rSec
  inc rMin
  cpi rMin,60
  brcs ClockTickEnd
  clr rMin
  inc rHr
  cpi rHr,24
  brcs ClockTickEnd
  clr rHr
ClockTickEnd:
  ret  

This is rather straight-forward. The two rjmp calls are similar to those in the high-level-language, only using the C flag is not available in Pascal (controller flags are forbidden and unknown areas in Pascal). Only assembler can use those, and uses those very often.

2.6 The time as string in Assembler

In the Pascal world, the result string is the result of a function. We don't know where this string will be located, but in assembler we'll have to know that exactly.

And something is different as well: as the AVR doesn't have command line window (he'd be too small for that), we can only prepare our string in SRAM and can look at it in a simulator, but we cannot see it in the chip. If we would add an LCD or a serial interface, we can make it appear there, so the ATtiny can display that to the big, wide world to see.

The assembler formulation of string conversion starts with a routine that can convert a byte to a string with leading zeros. This routine subtracts ten as often until the carry flag is set (underflow, subtraction repeated with brcc). The counter, here: rmp2, that counts how often this ends with a cleared carry, starts with the ASCII character '0' - 1, which is 47, just because if a number smaller than ten runs through the routine once, an ASCII-'0' is resulting. The assembler programmer uses this trick to save unnecessary instructions. His counter doesn't start with zero, so he can save one instruction with adding ASCII-'0' to the result. And he doesn't start with ASCII-'0' to save one DEC at the end. Did I already write that he is a crazy instruction-reducer nerd?

Pascal programmers do not even know how much instructions their code will produce, so they aren't in danger to think about any optimization of their code.

When storing the tens and the ones in SRAM, the assembler programmer loves to use pointers. Here we use the register pair Z. It consists of R31 (ZH, MSB) and R30 (ZL, LSB). With "ST Z+,rmp" this writes the content of the register to the location Z, and increases the pointer by one immediately after this write, so he points to the next location.

That's how the source looks like for the string conversion (ignore the lower line in the SRAM display: that is where the stack is at its work): The converted time in SRAM

; SRAM position for the time string
.def rmp2 = R20 ; needed for conversion
;
.dseg
sClockString:
.byte 15 ; "Time = 00:00:00"
;
; Code for time conversion
.cseg
; Byte in rmp as ASCII-string with leading zeros to Z
Int2Str:
  ldi rmp2,'0'-1
Int2Str1:
  inc rmp2
  subi rmp,10
  brcc Int2Str1
  st Z+,rmp2
  subi rmp,-'0'-10
  st Z+,rmp
  ret
; Convert time to string in Sram
Clock2String:
  ldi ZH,High(sClockString)
  ldi ZL,Low(sClockString)
  ldi rmp,'T'
  st Z+,rmp
  ldi rmp,'i'
  st Z+,rmp
  ldi rmp,'m'
  st Z+,rmp
  ldi rmp,'e'
  st Z+,rmp
  ldi rmp,' '
  st Z+.rmp
  ldi rmp,'='
  st Z+,rmp
  ldi rmp,' '
  st Z+,rmp
  mov rmp,rHr
  rcall Int2Str
  ldi rmp,':'
  st Z+,rmp
  mov rmp,rMin
  rcall Int2Str
  ldi rmp,':'
  st Z+,rmp
  mov rmp,rSek
  rcall Int2Str
  ret

This is considerably larger than in Pascal, just because all text requires two instructions per character. We could shorten this by using a flash-memory-based string for "Time = ", but that is more for the advanced course.

3 Comparing the executables of the Pascal- and AVR-Asm-programs

Here in this table you'll see the main properties of the two executables that are produced by FPC and the assembler.

ParameterPascal clockAVR-ASM-clock
Clock rate1,8 GHz1 MHz
Duration of one day4 ms1.30 s
from that: duration per tick46.30 ns15.05 µs
dto. per MHz clock rate83.33 µs
Sizewith FPC: 115 kBwith gavrasm: 71 Words (= 142 Bytes)


The following is surprising:
  1. The clock rate of my laptop is by 1,800-fold higher than that of a standard-AVR. Well, we can clock the AVR with a higher frequency, but what should it be good for? Except for reaching the battery-empty case a bit earlier ...
  2. My laptop absolves the 86,400 seconds of a day in 4 ms, for which the AVR needs slightly more than a second. But one second per day isn't a really long duration and rather less than one single peanut (0.0015% of the total time of a day).
  3. Per tick we can calculate for the laptop 46.3 ns, for the AVR 15.05 µs. This is by 300-fold faster.
  4. But, if my laptop would have the same clock rate as the AVR, he would need 83 µs, which is more than the 6-fold than an AVR. From that you can see that Pascal needs much longer than assembler.
  5. And at the end the size of the executable. The FPC is nearly 1000-fold as large as the assembler-generated exe. A lot of bytes for the command line output and for the storage management, but nothing really useful. What a Pascal executable just needs, to perform the same like the small flash executable.
And? Did I promise too much in stating that assembler source code in microcontrollers feels more at home than high-level languages? And that it makes sense to learn assembler as THE language for micros.

If you are on a PC or laptop, use what you want, all is fast and doesn't come near to limitations of storage spaces. But: in a micro, where you have eight different interrupt sources at work and where you have to react within microseconds to such events: do not try that with whatever high-level language, you'll fail. Even a 1.8 GHz laptop isn't fast enough to guarantee that. Assembler in an AVR can handle that very easy, and without any extreme cautions to be programmed - besides the ones that are basic to assembler (see the above example with the clock setting while ticking it in another code section).

No person that is healthy will use assembler on a PC or laptop with an operating system that provides most of the necessary functions. But an AVR hasn't an operating system, its 4 kB large flash does not offer enough space to waste your (and its) time with an operating system for AVRs: it is superfluous and worth less than nothing.

So, if you don't speak assembler: start learning it, play with it, learn how to get the internal hardware to work, solve self-defined problems, and you will see, what is possible without the fences, that high-level languages build around programmers. Do not fill the flash space with scrap, like compilers produce it, use it for meaningful content only. Assembler helps you to understand your controller, high-level languages only keep you away from that understanding and, instead, convert you to the slaves of libraries.

To the page top

©2022 by http://www.avr-asm-tutorial.net