Docsity
Docsity

Prepare-se para as provas
Prepare-se para as provas

Estude fácil! Tem muito documento disponível na Docsity


Ganhe pontos para baixar
Ganhe pontos para baixar

Ganhe pontos ajudando outros esrudantes ou compre um plano Premium


Guias e Dicas
Guias e Dicas


Iniciação em programação Assembler AVR, Manuais, Projetos, Pesquisas de Matérias técnicas

Texto em inglês sobre programação em Assembler de microcontroladores AVR

Tipologia: Manuais, Projetos, Pesquisas

2020

Compartilhado em 13/02/2020

ordam1
ordam1 🇧🇷

3 documentos

1 / 33

Toggle sidebar

Esta página não é visível na pré-visualização

Não perca as partes importantes!

bg1
Path: Home => AVR overview => starter steps
Learning AVR assembler - the very first steps
There are lots of short or long introductions to AVR assembler. The following was written
for absolute beginners who want to know why they should learn assembler and what the
first steps of learning this are.
1st step: Forget all you know about programming that you
learned so far
If you already know programming languages such as C, Pascal, diverse Basic dialects,
Java or whatever: kick that all aside, it makes no sense in assembler and blocks your first
steps in learning that. One basic rule in learning theory is: learning something that is re -
ally new starts with destroying previous or old knowledge. And even if it hurts: without
that you'll make no progress in learning, instead you'll always will stay at: "Why wasn't
that made like I always used to do things?" and "That's too complicated for me, I'll never
learn that." Previous or old knowledge stands in your way and blocks you from learning
new things.
So, let us first see what our standard view on computers is like. And then learn why mi-
cro-controllers are a different world.
1.1 Our standard approach to programming languages
Why that? Now, assembler is very, very different from other languages and even though
programs that run on a micro-controller still are bits and bytes, but the generation process
is very different.
Page 1 of 33
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21

Pré-visualização parcial do texto

Baixe Iniciação em programação Assembler AVR e outras Manuais, Projetos, Pesquisas em PDF para Matérias técnicas, somente na Docsity!

Path: Home => AVR overview => starter steps

Learning AVR assembler - the very first steps

There are lots of short or long introductions to AVR assembler. The following was written for absolute beginners who want to know why they should learn assembler and what the first steps of learning this are.

1st step: Forget all you know about programming that you

learned so far

If you already know programming languages such as C, Pascal, diverse Basic dialects, Java or whatever: kick that all aside, it makes no sense in assembler and blocks your first steps in learning that. One basic rule in learning theory is: learning something that is re - ally new starts with destroying previous or old knowledge. And even if it hurts: without that you'll make no progress in learning, instead you'll always will stay at: "Why wasn't that made like I always used to do things?" and "That's too complicated for me, I'll never learn that." Previous or old knowledge stands in your way and blocks you from learning new things. So, let us first see what our standard view on computers is like. And then learn why mi - cro-controllers are a different world.

1.1 Our standard approach to programming languages

Why that? Now, assembler is very, very different from other languages and even though programs that run on a micro-controller still are bits and bytes, but the generation process is very different.

These are the four levels of a usual computer. We usually see only the red level outside of the onion: we start an office software (that produces a picture of an empty sheet of paper on the screen), we click on a key on the keyboard and the letter written on the key appears in the picture. We deal with the outer level, what happens below that is in-trans- parent and we need not to know what goes on there as long as the application software works as de- sired. Each of the above actions is asso- ciated with thousands of steps of the inner level, the Central Pro- cessing Unit or CPU. Our click onto the keyboard involves those thousands of steps, until the key- board driver has found out which key has been pressed and what letter is written on the key. The keyboard driver hands out the letter to the application software, that stores it and sends 1,000s of commands to the display screen driver to change the picture of the empty sheet, that now shows the letter of the key pressed. Fortunately the millions of CPU actions associated with that need by far less than a second so that we do not have to wait for long to see if it works.

1.2 Programming in high-level languages

So what changes if we do programming in C, Pascal, Basic or Java? We type in commands in that language in an editor, that stores a source code file on the disk. Then we call a so- called compiler, that reads the source code and translates it to a series of commands, packs those into an executable file, makes an application program from it and stores it on the disk. If we start that by clicking on it, the compiled commands are executed. More than 90% of the executable are calls to lower levels such as to the operating system and very seldom to drivers. Some manipulate memory spaces, via the memory adminis- tration driver. Nearly none of those directly address the CPU. How the source code com- piles is completely in-transparent for the programmer: he needs only to know if the pro- duced executable works as desired, not how the executable does this. The compiler de- cides over this and does not ask the programmer if he should do it in this or another way. Therefore usual programming in high-level languages does neither need to nor does it fac- tually increase our knowledge and understanding of how the computer finally works. We are still dump users even though we know how to program (and with that solve our appli- cation needs).

  1. The bit combination read is then decoded: certain bit combinations do add two numbers, another one subtracts, another one ANDs or ORs or EXORs them, etc. Some CPUs know also more complex operations, such as writing bit content to a lo- cation using a pointer register, and auto-incrementing that pointer register after- wards. What the CPU can do is called its instruction set. So decoding the input bits has to translate the instruction bits to CPU operations.
  2. The decoded actions are then performed, e. g. two numbers are added and the re- sult is written to a certain location. If during adding an overflow occurs, a flag called CARRY in the CPU's flag register is set to one, otherwise this flag is cleared.
  3. The CPU then increases its PC and restarts with step 1 above. Even though very different from their design, speed and capabilities all CPUs work in this way.

2.2 Specifics of the AVR CPUs

Now to the more specific properties of the AVR’s CPU. The largest difference to all other CPU types is that the AVRs have a lot of registers: 32. Registers are bit storage locations with a size of 8 bits that the AVR CPU can directly use. So the CPU can add any one of the 32 registers with any other one (even with itself) and can store the result in any one of these 32 registers. So the bit combination of the instruction word (16 bits) for adding looks like this: So, whenever the AVR CPU fetches 16 bits, of which the highest four are zeros and the next two following are ones, she knows what to do:

  1. Convert the r-rrrr bits to a number that points to register 0 to 31 and place its con- tent to its adder input.
  2. Convert the ddddd bits to a number that points to another register between 0 and 31, place its content to the second adder input, and add these two bytes.
  3. Write the result, the sum of both, to the register that r-rrrr points to by replacing its previous content.
  4. If an overflow occurs (the result is larger than what can be fitted into an 8 bit regis- ter), the CPU's so-called status register SREG gets a one into its bit zero, called CARRY or "C" flag. Otherwise this bit is cleared. If the result is zero, it sets bit 1 of SREG. which is the ZERO or "Z" flag. Whatever follows can use those flags, as long as no other instruction alters those flags. Don't be afraid, you don't have to learn all 16 bit opcodes. There are ways to simplify those: with mnemonics. The mnemonic for 0000,11xx,xxxx,xxxx is simply ADD , the two registers r-rrrr and ddddd follow as parameters. The complete instruction would therefore be: ADD r,d with r and d representing numbers between 0 and 31. A simple rule: the first parameter is always the register where the result is written to! AVR CPUs know more than 100 different instructions, each of those having a specific op- code (that the CPU can decode) and a mnemonic (that the human programmer can use). If you forgot a mnemonic you can search it in every data sheet of the respective AVR type

in the chapter Instruction Set Summary , if you really need to know the binary opcode search for the document Instruction Set Manual on ATMEL/Microchip's website. More specific to the AVRs is that the CPU can only fetch opcodes that are stored in the flash memory of the chip. No external or opcodes stored in SRAM or in EEPROM can be ex - ecuted. So if you want the CPU to execute instructions, write those to the flash memory.

2.3 Assembler is the language of the CPU?

Well, yes and no. Exactly is that the 16-bit opcodes are the language of the CPU. The mnemonics, such as ADD, are assembly language. A program also named assembler converts the mnemonics to opcodes: it "assembles" the bits 0000,11 and the two num- bers of the two registers in binary form and produces the binary output, to be pro- grammed to the flash memory for execution by the CPU. As the language assembler only consists of mnemonics for the CPU's binary opcodes, it is as near as possible to the CPU. So assembler is called a "hardware-near" language, in contrast to languages that are far away from the hardware, in their own world.

2.4 A controller instead of a computer

Other than the computer onion as shown above very different hardware is already inte - grated in a controller's CPU core:

  • memory: flash memory for executable instructions, static RAM for storage of data and as stack, EEPROM for storing durable content,
  • handling and controlling of in- and output pins,
  • starting, stopping and controlling of timers and counters,
  • enabling a comparison between two analog voltages and reading the comparison result as a bit,
  • connecting input pins to an AD converter that converts an analog voltage to a num- ber, using either the operating voltage, an externally supplied voltage or a constant internal voltage as reference,
  • serial interfaces, such as synchronous or asynchronous communication devices, with the necessary baud rate generators and dividers,
  • means to manipulate clock rates, via fuses or via programmable dividers,
  • and much, much more. Therefore our controller looks rather like this. It combines a CPU with the hardware layer, but lacks the driver, the operat- ing system and the application software layer. And, with all that hardware integrated, another feature of computers is already build into these controllers: an interrupt controller. This allows to enable interrupts for all integrated hardware, to prioritize those interrupts and to interrupt pro- gram execution whenever hardware needs the attention of the CPU. And this already very fast: within micro-seconds a reac- tion on a hardware event is possible. Try to do that with a PC or notebook: even though fast enough, the operating system is not designed to react fast on such events. You'll be lucky if it reacts in the milliseconds range, but not that fast like a
  • adds the step that stores the result in the memory storage place for the variable C. The programmer does not need to know
  • where A, B and C will be stored,
  • what function exactly will be used for adding, and
  • at which memory address the execution of adding happens. All this is solely up to the compiler. Not so in assembler. Your program looks like this: ; Add two 8-bit numbers in R1 and R2, result in R mov R3,R1 ; Copy #1 to R add R3,R2 ; Add #2 to R3 and store the result in R This is very different from C=A+B:
  • You'll have to decide exactly where the variables A, B and C are stored, either
  1. in registers like here, of which AVRs have 32 8-bit called R0 to R31 (PICs have only one), and in which of those,
  2. in SRAM memory, and at which addresses: in that case the two numbers have first to be transferred to registers, because the CPU can only add regis- ters. Wherever you store these, it is up to the programmer to decide. And it is his re- sponsibility to protect this space, too. If not be needed any more, he can overwrite this. Nobody and no compiler warns you if you overwrite the content of this storage space. The freedom of placement is associated with the responsibility to protect (as long as needed) and the risk of erroneous actions.
  • While in the high-level language the "+" initiates adding, you'll have to write "ADD", which is a CPU instruction. While behind "+" a large number of CPU instructions can follow, depending from the size and type of A, B and C, the "ADD" here is exactly and only one instruction or CPU command, nothing else than that.
  • If you'll add two 8 bit numbers, your result can be larger than 255. That would re- quire an additional register for the result because it would not fit into only one. So either reserve R4 for that or do something else if the result is larger than 255. If you reserved R4 for that, just insert the instruction clr R4 after the mov R3,R and add adc R4,R4 at the end and your source code above looks like that: ; Add two 8-bit numbers in R1 and R2, result in R4:R mov R3,R1 ; Copy #1 to R clr R4 ; Clear the upper byte of the result add R3,R2 ; Add #2 to R3 and store the result in R adc R4,R4 ; Add the carry flag to the empty upper byte Please do not ask me why the mnemonic for copying a register is not COPY but MOV , even though nothing is "moved" here (the source will not be empty, other than the term "move" implies).
  • In a high-level-language the try to store a result larger than 255 into a variable de- fined as an 8-bit byte throws an exception (overflow). If so enabled, the exception stops the program from further execution and displays an error message. It is up to the programmer, in a high-level language as well as in assembler, to care for such overflows. But in assembler nothing happens if you ignore a carry flag being set by an ADD instruction: nothing happens if you do not program that. If you want to stop further execution you'll have to program that by yourself, because there is no overlying operating system that can display an error message. So the basic differences between high-level languages and assembler are:
  1. Assembler programmer can only use instructions that the CPU knows and performs. More complex functions, such as adding two 64 bit numbers, have to be written by the assembler programmer.
  2. Locations of variables have to be explicitly defined in assembler. Only the content of registers can be treated by the CPU, the register location has to be explicitly de- fined in the instruction. No unknown, undefined or in-transparent positioning of variables in assembler.
  3. Any available storage space can be used by the assembler programmer, the respon- sibility to protect content from overwriting remains with the programmer, no pro- tection is automatically working.
  4. Storage spaces can be combined in any possible manner in assembler: if you need eight bytes for a 64-bit number, just place them either in eight registers or in eight SRAM storage cells. Row and content are up to you, no compiler disturbs your own design considerations. In high-level languages the programmer has no choice, he has to take what he gets and has no chance to decide something.
  5. A central category in high-level languages are types of variables and constants. Those type definitions limit automatically what can be done with a variable. Assem- bler does not know any types of variables or constants, all are bits and bytes. No limitations are given on what can be done with those bits and bytes. The freedom to add three to an ASCII character stored in a register, by that making a 'D' from an 'A', is a simple one-line instruction in assembler. Try to formulate this in a high-level language, you'll end up in an orgy (Pascal: c:=SUCC(SUCC(SUCC(c))) or c:=CHAR(BYTE(c)+3))!. In assembler this is simply SUBI R16,-3 if the ASCII char- acter is in register R16. In AVR assembler there is no ADDI, so you'll have to
  6. All protectional features, such as detecting overflows, under-flows, division by zero, etc., have to be explicitly programmed by the assembler programmer. This is not a large difference as high-level languages provide tools to cope with that, but most programming languages just stop their execution, should this occur. These here are the No-No's in assembler - forget these high-level constructions for now:
  • High-level languages such as Java, C or Pascal allow to define functions, allow to call those with parameters in brackets and those return a result in one of the types of the language. Simply forget this: a CALL to a function in assembler can use any storage location, and the result of the CALL can also use any location.
  • We already learned that type definitions make no sense in assembler because any- thing is in bits and bytes in the different locations. All structuring of data has to be made by the assembler programmer, and if he cares not enough for his own rules, he alone suffers from bugs. So forget type definitions.

As you can simply see, anything, and even more than that, can be done in assembler, too. More than that means: you can change the step width in between, you can manipulate the counter in between the loop (which is not allowed in several high-level languages), you can test if additional conditions are met (e. g. if a port-pin has changed polarity in bet- ween) that should stop the loop, you can even jump to any location outside the loop should a certain condition require that. Even if your loop is nested (another loop inside an outer loop) you can jump to where you want in assembler. All features are allowed in as - sembler, no limitations. But: increased freedom means increased responsibility. If your counter counts with zero steps up or down, the AVR will accept that and your loop will never end. It is up to you to find that bug and replace it with something that functions well. As you can see: better forget the FOR/NEXT construction and use whatever is the appro- priate method for your needs. 3 Simulating the hardware of micro-controllers As AVRs do not have hardware that can output a screen picture we can hardly see what happens if we program our first assembler routines. So we have to have another way to see what is going on.

3.1 The hardware to be simulated

As we saw a micro-con- troller (here some older types) is a little bit dif- ferent from a computer: it is a naked CPU with some additional hardware packed around it. Small storages, such as a flash memory with a few kilo-bytes or a static RAM with also a few bytes or kilo-bytes, and an even smaller EEPROM with only a few bytes. But: all that needs less than one per-mill of the current or power of a computer, and can be operated for hours, days and months from a small battery. The available hardware consists of

  1. a flash memory storage for the running program, and
  2. a SRAM memory that looses its content when the operating voltage falls below a minimum value (in that case all bits will fall back to ones), and
  3. an EEPROM storage, that keeps its content for some years even in absence of an operating voltage, and
  4. lots of additional small machines such as timers/counters, AD converters, etc., and first of all
  5. a rather large number of pins that can be switched on and off by the program in the AVR's flash memory. By means of the last named hardware we could place information (bits and bytes) on these pins and by adding some LEDs and resistors we could see if a bit is zero or one. But that is not a very transparent mode, e. g. because we do not see very fast changes and we cannot reduce the speed of the controller.

3.2 A simulator as a do-as-if-controller

A simulator is an application program for a computer that behaves like a micro-controller. Other than the original, it allows to single-step through the assembler program: it exe- cutes an instruction, sets CPU flags, displays register content and displays SRAM or EEP - ROM content, and then stops. Before doing the next step we have all the time we need to read and understand what the instruction has done. The simulator avr_sim is available for free, can be downloaded as executable for 64-bit Linux and Windows versions or can be compiled using Lazarus from the Pascal source code that is also provided for many other operating systems. Each version that can be down- loaded (just use the latest one) comes with a handbook that describes all features of the simulator. So if you need other information than described here, consult the handbook. One special thing to learn here is that information on registers and other controller inter- nals is displayed in hexadecimal format. A hexadecimal digit takes four single bits and dis- plays those as 0 to 9 (0000 to 1001) or as A to F (1010 to 1111). So 16-bit numbers are represented by four hexadecimal digits. Use a programmer calculator to convert hexadeci- mal to decimal if you need it.

3.3 Our first assembler program

Start avr_sim, it should come up with an empty editor field to the right, with an empty project field to the left and a menu on the top.

The first lines that are not comments are the following: .nolist .include "tn13adef.inc" ; Define device ATtiny13A .list These are three directives, starting with a ".": those are commands for the assembler. The first switches the output in the list file off, the third one on again. The second directive in - cludes a file named "tn13adef.inc". This file holds addresses and constants for the device and switches a check on if the instructions are valid in that device type. But the file is not really read in avr_sim: all those files are implemented in avr_sim, so no extra file is nec - essary. After that two additional directives follow: .cseg instructs the assembler to produce code (code sgment), .org 000000 set the address for which the code will be assembled. As the device starts at address 0 when the operating voltage is applied, this ensures that our code is executed. In the Main program section, the first entry is a label: Main:. Such labels all end with a ":" and tell the assembler to associate the label's name with the current address (here: 000000). The label is followed by another label called Loop: and a line saying rjmp loop. This con- struction is an indefinite loop: the rjmp means "Relative Jump" and causes the controller to jump back to the label Loop. The rjmp is a jump instruction. We assemble this source code by clicking on the Assemble entry on the menu line.

Assembling opens a window, that displays the progress during assembling. Here, the gavrasm assembler is at work. You can ignore the displayed messages, but not the follow- ing window that reports success. The editor win- dow now dis- plays the listing that the assem- bler produced. It holds all rele- vant informa- tion. The most relevant here is the line that is in the red box: the rjmp loop has been trans- lated by the as- sembler to an instruction at address 000000. The instruction is hexadecimal CFFF. If we would pro- gram the hex file myfirst.hex to the flash memory of a de- vice, the con- troller forever jumps back to address 000000, and does nothing else. In case you need it: the symbol table at the end of the listing shows all relevant informa- tion on constants, labels and all other symbols. The labels Main and Loop, for example, point to address 000000. To add another instruction before the controller is send into the indefinite loop, we add the two lines before the loop label: mov R3,R1 ; Copy number 1 to register R add R3,R2 ; Add number 2 to register R

The registers now show that their con- tent has changed: R and R2 are now one, the adder result is 2. As each step sets the previous value the fact that all three registers are yellow was reached by setting a breakpoint on the rjmp (set the cursor to that line, right-click and select Break- point , the breakpoint is displayed as B in that line) and starting the simulation with the Run entry in the menu. So all changes to registers remain yellow.

3.4 Using the flags

If we exchange the line inc R1 by dec R1 we provoke some new effects. The DEC de- creases the content of register R1. The zero yields a 255 or hexadecimal FF. As the result will be larger than 255 we also add R4 for the higher byte. The source code now: dec R1 ; Number 1 to 255 inc R2 ; Number 2 to 1 clr R4 ; Result MSB to zero mov R3,R1 ; Copy number 1 to R add R3,R2 ; Add number 2 adc R4,R4 ; Add carry to MSB Loop: rjmp Loop The CLR instruction clears the regis- ter to all zeros. If we now execute the first five in- structions, before the adc, we'll get the following register content. The two numbers FF and 01 are ok, adding those has yielded zero in R3. But: in contrary to our previous adding operations the C flag in the status register SREG is now 1, indicating that the adding has caused an overflow or CARRY. And: as the result is zero the Z (or zero) flag is also set one. The next instruction, adc R4,R4 now adds the content of R4 with itself (which is zero) and the carry flag. As the carry flag is set, the result is 1.

That is what the flags are for: they indicate relevant states that the CPU detects, but can also be set or cleared by the programmer (the carry flag with the instructions SEC or CLC ). Another use of the flags are conditional branches. The instructions BRCS label or BRCC label jump to a label if the carry flag is one (SET) of zero (CLEAR). The same example can also be formulated like this: dec R1 ; Number 1 to 1 inc R2 ; Number 2 to 1 clr R4 ; Result MSB to zero mov R3,R1 ; Copy number 1 to R add R3,R2 ; Add number 2 brcc Loop ; Branch if carry is clear inc R4 ; Increase MSB Loop: rjmp loop That has the same effect: the increase of the MSB is not executed if the carry flag is not set. Note that not all instructions change flags. If and which flags are altered by the CPU can be seen from the Instruction Summary in the data-book of the device. So, e. g. the INC instruction sets the Z flag, but not the C flag. Consult the data-book or the Instruction Set Manual if you are not sure or in doubt. With these instruments now learned we can easily construct a 64 bit counter. We increase the lowest byte in R1 and each time, when the zero flag is set, we increase the next higher byte in R2, too. And so on for the bytes up to R8. The source code for that: Count: inc R0 ; Increase the lowest byte brne Count ; If Z flag is clear count on inc R1 ; Increase the second byte brne Count ; If Z flag is clear count on inc R2 ; Increase the third byte brne Count ; If Z flag is clear count on inc R3 ; Increase the fourth byte brne Count ; If Z flag is clear count on inc R4 ; Increase the fifth byte brne Count ; If Z flag is clear count on inc R5 ; Increase the sixth byte brne Count ; If Z flag is clear count on inc R6 ; Increase the seventh byte brne Count ; If Z flag is clear count on inc R7 ; Increase the highest byte rjmp Count ; Count on

4.1 Setting and clearing single I/O bits

An I/O pin can be input or output. This is controlled by clearing or setting their bit in the port register DDRp. As the ATtiny13 has only one I/O port and only five I/O pins, you can use the following instructions: Loop: cbi DDRB,DDB0 ; Clear direction bit of pin PB sbi DDRB,DDB0 ; Set direction bit of pin PB rjmp Loop This makes the external pin PB0 first input ( CBI means Set Bit I/O) and then output ( CBI means Clear Bit I/O), then restarts again. If you would attach a LED to external pin PB and tie it with a resistor of a few 100 Ohms to the operating voltage, the LED would go on (as long as the output is active) and off (as long as the output is not active). If you as - semble that code, simulate it step-by-step you'll see the effect in the port view window. A second possibility is to make PB0 an output permanently and to clear and set the PORTB bit. The source code for that: sbi DDRB,DDB0 ; Make PB0 an output Loop: cbi PORTB,PORTB0 ; Make PBO low sbi PORTB,PORTB0 ; Make PB0 high rjmp Loop ; Repeat all over Now the out- put pin PB always drives the output pin: first to low (an at- tached LED to the operating voltage is on), then to high (the LED is off). That is what you get on the PB0 pin: a

very fast signal of a few micro-seconds duration, the low period being half as long as the high period. BTW: you'll get those pulse dis- plays in avr_sim if you enable Scope , then in the scope control window choose these selections and click Show scope. A click on Time elapsed in the simulation status window clears the scope and restarts the time. An additional case occurs if you clear the direction bit in a port bit and set the port output bit: The port pin now switches an internal resistor of approximately 50 kΩ to the operatig voltage to the input pin. This ties the input pin to the operating voltage ("pull-up"), so reading it by the instruction IN R16,PINB shows a one at this bit location. If you connect a switch or a button to this input pin, you can read a zero at this bit if the switch is on, tieing the input pin to ground and over-riding the pull-up, or a one if the switch or button is off.

4.2 Execution times

To exactly find out,

  1. at which frequency the pin toggles, and
  2. what causes the 66.7% pulse width we have to go deeper into the details of instruction execution of the AVRs. Each instruction in an AVR requires one, some require two clock cycles. At a clock rate of 1.2 MHz, on which the ATtiny13 works by default, each single clock cycle is 1 / 1.2 MHz = 0.833 μs long. The instructions in our PB0-toggle program are these clock cycles long: sbi DDRB,DDB0 ; Make PB0 an output, 2 clock cycles Loop: cbi PORTB,PORTB0 ; Make PBO low, 2 clock cycles sbi PORTB,PORTB0 ; Make PB0 high, 2 clock cycles rjmp Loop ; Repeat all over, 2 clock cycles So our loop is 6 clock cycles long. The frequency is therefore frectangle = 1.2 MHz / 6 = 200 kHz