Path:
Home =>
AVR-EN =>
Assembler introduction => Comparision hi-lo
(Diese Seite in Deutsch:
)
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:
- in three of its 32 registers, or
- 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:
- 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".
- 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):
; 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.
Parameter | Pascal clock | AVR-ASM-clock |
Clock rate | 1,8 GHz | 1 MHz |
Duration of one day | 4 ms | 1.30 s |
from that: duration per tick | 46.30 ns | 15.05 µs |
dto. per MHz clock rate | 83.33 µs |
Size | with FPC: 115 kB | with gavrasm: 71 Words (= 142 Bytes) |
The following is surprising:
- 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 ...
- 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).
- Per tick we can calculate for the laptop 46.3 ns,
for the AVR 15.05 µs. This is by 300-fold faster.
- 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.
- 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