Path: Home => AVR-overview => Assembler intro => assembler concept

The concept behind the language assembler in microcontrollers

Attention! This page is on programming microcontrollers, not on PCs with Linux- or Windows operating systems and similar elefants, but on small mices. It is not on programming ethernet mega-machines, but on the question why a beginner should start with assembler and not with a complex high-level language.

This page shows the concept behind assembler, what those familar with high-level languages have to give up to learn assembler and why assembler is not machine language.

The hardware of microcontrollers

What has the hardware to do with assembler? Much, as can be seen from the following.

The concept behind assembler is to make the hardware resources of the processor accessable. Resources means all hardware components, like Accessable means directly accessable and not via drivers or other interfaces, that an operating system provides. That means, you control the serial interface or the AD converter, not some other layer between you and the hardware. As award for your efforts, the complete hardware is at your command, not only the part that the compiler designer and the operating system programmer provides for you.

To the top of this page

How the CPU works

Most important for understanding assembler is to understand how the CPU works. The CPU reads instructions (instruction fetch) from the program storage (the flash), translates those into executable steps and executes those. In AVRs, those instructions are written as 16 bit numbers to the flash storage, and are read from there (first step). The number read then translates (second step) e.g. to transporting the content of the two registers R0 and R1 to the ALU (third step), to add those (fourth step) and to write the result into the register R0 (fifth step). Registers are simple 8 bit wide storages that can directly be tied to the ALU to be read from and to be written to.

The coding of instructions is demonstrated by some examples.
CPU operationCode (binary)Code (hex)
Send CPU to sleep1001.0101.1000.10009588
Add register R1 to register R00000.1100.0000.00010C01
Subtract register R1 from register R00001.1000.0000.00011801
Write constant 170 to register R161110.1010.0000.1010EA0A
Multiply register R3 with register R2 and write the result to registers R1 (MSB) and R0 (LSB)1001.1100.0011.00109C32
So, if the CPU reads hex 9588 from the flash storage, it stops its operation and does not fetch instructions any more. Don't be afraid, there is another mechanism necessary before the CPU executes this. And you can wake up the CPU from that.

If the CPU reads hex 0C01, R0 and R1 is added and the result is written to register R0. This is executed like demonstrated in the picture.

Executing instructions First the instruction word (16 bit) is read from the flash and translated to executable steps.

The next step connects the registers to the ALU inputs, and adds their content.

Next, the result is written to the register.

If the CPU reads hex 9C23 from the flash, the registers R3 and R2 are muliplied and the result is written to R1 (upper 8 bits) and R0 (lower 8 bits). If the ALU is not equipped with hardware for multiplication (e.g. in an ATtiny13), the 9C23 does nothing at all. It doesn't even open an error window (the tiny13 doesn't have that hardware)!

In principle the CPU can execute 65,536 (16-bit) different instructions. But because not only 170 should be written to a specific register, but values between 0 and 255 to any register between R16 and R31, this load instruction requires 256*16 = 4,096 of the 65,536 theoretically possible instructions. The direct load instruction for the constant c (c7..c0) and registers r (r3..r0, r4 is always 1 and not encoded) is coded like this:
Bit15141312111009080706050403020100
Content1110c7c6c5c4r3r2r1r0c3c2c1c0
Why those bits are placed like this in the instruction word remains ATMEL's secret.

Addition and subtraction require 32*32 = 1,024 combinations and the target registers R0..R31 (t4..t0) and source registers R0..R31 (s4..s0) are coded like this:
Bit15141312111009080706050403020100
Add content000011s4t4t3t2t1t0s3s2s1s0
Subtract content000110s4t4t3t2t1t0s3s2s1s0
Please, do not learn these bit placements, you will not need them later. Just understand how an instruction word is coded and executed.

To the top of this page

Instructions in assembler

There is no need to learn 16-bit numbers and the crazy placement of bits within those, because in assembler you'll use human-readable abbreviations for that, so-called mnemonics, an aid to memory. The assembler representation for hex 9588 is simply the abbreviation "SLEEP". In contrast to 9588, SLEEP is easy to remember. Even for someone like me that has difficulties in remembering its own phone number.
Adding simply is "ADD". For naming the two registers, that are to be added, they are written as parameters. (No, not in brackets. C programmers, forget those brackets. You don't need those in assembler.) Simply type "ADD R0,R1". The line translates to a single 16 bit word, 0C01. The translation is done by the assembler.

The CPU only understands 0C01. The assembler translates the line to this 16 bit word, which is written to the flash storage, read from the CPU from there and executed. Each instruction that the CPU understands has such a mnemonic. And vice versa: each mnemonic has exactly one corresponding CPU instruction with a certain course of actions. The ability of the CPU determines the extent of instructions that are available in assembler. The language of the CPU is the base, the mnemonics only represent the abilities of the CPU itself.

To the top of this page

Difference to high-level languages

Here some hints for high-level programmers. In high-level languages the constructions are not depending from the hardware or the abilities of a CPU. Those constructions work on very different processors, if there is a compiler for that language and for the processor family available. The compiler translates those language constructions to the processor's binary language. A GOTO in Basic looks like a JMP in assembler, but there is a difference in the whole concept between those two.

A transfer of program code to another processor hardware does only work if the hardware is able to do the same. If a processor CPU doesn't have access to a 16 bit timer, the compiler for a high-level language has to simulate one, using an 8-bit timer and some time-consuming code. If three timers are available, and the compiler is written for only two or a single timer, the available hardware remains unused. So you totally depend on the compiler's abilities, not on the CPU's abilities.

Another example with the above shown instruction "MUL". In assembler, the target processor determines if you can use this instruction or if you have to write a multiplication routine. If, in a high-level language, you use a multiplication the compiler inserts a math library that multiplies every kind of numbers, even if you have only 8-by-8-bit numbers and MUL alone would do it. The lib offers an integer, a long-word and some other routines for multiplications that you don't need. A whole package of things you don't really need. So you run out of flash in a small tiny AVR, and you change to a mega with 35 unused port pins. Or an xmega, just to get your elefant lib with superfluous routines into the flash. That is what you get from a simple "*", without even being asked.

To the top of this page

Assembler is not machine language

Because assembler is closer to the hardware than any other language, it is often called machine language. This is not exact because the CPU only understands 16 bit instruction words in binary form. The string "ADD R0,R1" cannot be executed. And assembler is much simpler than machine language. Similarities between machine language and assembler are a feature, not a bug.

Interpreting and assembler

With an interpreter the CPU first translates the human-readable code into binary words that can be exectuted then. The interpreter would In the consequence, probably a simple machine code like "ADD R0,R1" (in Assembler) would result. But most probably the resulting machine code would be multiple words long (read and write variables from/to SRAM, 16-bit-integer adding, register saving/restoring on stack, etc., etc.).
The difference between the interpreter and the assembling is that, after assembling, the CPU gets its favored meal, executable words, directly. When interpreting the CPU is, during most of the time, performing the translation task. Translation probably requires 20 or 200 CPU steps, before the three or four words can be executed. Execution speed so is more than lame. While this is no problem if one uses a fast clock speed, it is inappropriate in time critical situations, where fast response to an event is required. No one knows what the CPU is just doing and how long this requires.
Not having to think about timing issues leads to the inability of the human programmer to resolve timing issues, and missing information on timing keeps him unable to do those things, if required.

To the top of this page

High level languages and Assembler

High level languages insert additional nontransparent separation levels between the CPU and the source code. An example for such an nontransparent concept are variables. These variables are storages that can store a number, a text string or a single Boolean value. In the source code, a variable name represents a place where the variable is located, and, by declaring variables, the type (numbers and their format, strings and their length, etc.).
For learning assembler, just forget the high level language concept of variables. Assembler only knows bits, bytes, registers and SRAM bytes. The term "variable" has no meaning in assembler. Also, related terms like "type" are useless and do not make any sense here.
High level languages require you to declare variables prior to their first use in the source code, e.g. as Byte (8-bit), double word (16-bit), integer (15-bit plus 1 sign bit). Compilers for that language place such declared variables somewhere in the available storage space, including the 32 registers. If this placement is selected rather blind by the compiler or if there is some priority rule used, like the assembler programmer carefully does it, is depending more from the price of the compiler. The programmer can only try to understand what the compiler "thought" when he placed the variable. The power to decide has been given to the compiler. That "relieves" the programmer from the trouble of that decision, but makes him a slave of the compiler.

To the top of this page

The instruction "A = A + B" is now type-proofed: if A is defined as a character and B a number (e.g. = 2), the formulation isn't accepted because character codes cannot be added with numbers. Programmers in high level languages believe that this type check prevents them from programming nonsense. The protection, that the compiler provides in this case by prohibiting your type error, is rather useless: adding 2 to the character "F" of course should yield a "H" as result, what else? Assembler allows you to do that, but not a compiler.
Assembler allows you to add numbers like 7 or 48 to add and subtract to every byte storage, no matter what type of thing is in the byte storage. What is in that storage, is a matter of decision by the programmer, not by a compiler. If an operation with that content makes sense is a matter of decision by the programmer, not by the compiler. If four registers represent a 32-bit-value or four ASCII characters, if those four bytes are placed low-to-high, high-to-low or completely mixed, is just up to the programmer. He is the master of placement, no one else. Types are unknown, all consists of bits and bytes somewhere in the available storage place. The programmer has the task of organizing, but also the chance of optimizing.
Of a similar effect are all the other rules, that the high level programmer is limited to. It is always claimed that it is saver and of a better overview to program anything in subroutines, to not jump around in the code, to hand over variables as parameters, and to give back results from functions. Forget most of those rules in assembler, they don't make much sense. Good assembler programming requires some rules, too, but very different ones. And, what's the best: most of them have to be created by yourself to help yourself. So: welcome in the world of freedom to do what you want, not what the compiler decides for you or what theoretical professors think would be good programming rules.
High level programmers are addicted to a number of concepts that stand in the way of learning assembler: separation in different access levels, in hardware, drivers and other interfaces. In assembler this separation is complete nonsense, separation would urge you to numerous workarounds, if you want to solve your problem in an optimal way.
Because most of the high level programming rules don't make sense, and because even puristic high level programmers break their own rules, whenever appropriate, see those rules as a nice cage, preventing you from being creative. Those questions don't play a role here. Everything is direct access, every storage is available at any time, nothing prevents your access to hardware, anything can be changed - and even can be corrupted. Responsibility remains by the programmer only, that has to use his brain to avoid conflicts when accessing hardware.
The other side of missing protection mechanisms is the freedom to do everything at any time. So, smash your ties away to start learning assembler. You will develop your own ties later on to prevent yourself from running into errors.

To the top of this page

What is really easier in assembler

All words and concepts that the assembler programmer needs is in the datasheet of the processor: the instruction and the port table. Done! With the words found there anything can be constructed. No other documents necessary. How the timer is started (is writing "Timer.Start(8)" somehow easier to understand than "LDI R16,0x02 and OUT TCCR0,R16"?), how the timer is restarted at zero ("CLR R16 and OUT TCCR0,R16"), it is all in the data sheet. No need to consult a more or less good documentation on how a compiler defines this or that. No special, compiler-designed words and concepts to be learned here, all is in the datasheet. If you want to use a certain timer in the processor for a certain purpose in a certain mode of the 15 different possible modes, nothing is in the way to access the timer, to stop and start it, etc.
What is in a high level language easier to write "A = A + B" instead of "MUL R16,R17"? Not much. If A and B aren't defined as bytes or if the processor type is tiny and doesn't understand MUL, the simple MUL has to be exchanged with some other source code, as designed by the assembler programmer or copy/pasted and adapted to the needs. No reason to import an nontransparent library instead, just because you're to lazy to start your brain and learn.
Assembler teaches you directly how the processor works. Because no compiler takes over your tasks, you are completely the master of the processor. The reward for doing this work, you are granted full access to everything. If you want, you can program a baud-rate of 45.45 bps on the UART. A speed setting that no Windows PC allows, because the operating system allows only multiples of 75 (Why? Because some historic mechanical teletype writers had those special mechanical gear boxes, allowing quick selection of either 75 or 300 bps.). If, in addition, you want 1 and a half stop bytes instead of either 1 or 2, why not programming your own serial device with assembler software. No reason to give things up.
Who is able to program in assembler has a feeling for what the processor allows. Who changes from assembler to a higher level language later on, e.g. in case of very complex tasks, has made the decision to select that on a rational basis. If someone skips learning assembler he has to do what he can, sticks to the available libraries and programs creative workarounds for things that the compiler doesn't allow, and in a way that assembler programmers would laugh at loud. The whole world of the processor is at the assembler programmer's command, so why do complicated and highly sensitive workarounds on something you can formulate in a nice, lean, esthetic way?

To the top of this page


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