This tutorial is a continuation of "AVR Assembler Tutorial 1"
If you haven't gone through Tutorial 1 you should stop now and do that one first.
In this tutorial we will continue our study of assembly language programming of the atmega328p used in Arduino's.
You will need:
- a breadboard Arduino or just a normal Arduino as in Tutorial 1
- an LED
- a 220 ohm resistor
- a push button
- connecting wires for making the circuit on your breadboard
- Instuction Set Manual: www.atmel.com/images/atmel-0856-avr-instruction-s...
- Datasheet: www.atmel.com/images/Atmel-8271-8-bit-AVR-Microco...
The complete collection of my tutorials can be found here: https://www.instructables.com/id/Command-Line-AVR-T...
Step 1: Building the Circuit
First you need to construct the circuit that we will be studying in this tutorial.
Here is the way it is connected:
PB0 (digital pin 8) ---> LED ---> R (220 ohm) ---> 5V
PD0 (digital pin 0) ---> pushbutton ---> GND
You can check that your LED is oriented properly by connecting it to GND instead of PB0. If nothing happens then reverse the orientation and the light should come on. Then reconnect it to PB0 and continue. The picture shows how my breadboard arduino is connected.
Step 2: Writing the Assembly Code
Write the following code in a text file called pushbutton.asm and compile it with avra as you did in Tutorial 1.
Notice that in this code we have plenty of comments. Every time the assembler sees a semicolon it will skip the rest of the line and go on to the next line. It is good programming practice (especially in assembly language!) to heavily comment your code so that when you return to it in the future you will know what you were doing. I am going to comment things quite a lot in the first few tutorials so that we know exactly what is going on and why. Later on, once we become a bit better at assembly coding I will comment things in a bit less detail.
;************************************ ; written by: 1o_o7 ; date: <2014|10|23> ; version: 1.0 ; file saved as: pushbutton.asm ; for AVR: atmega328p ; clock frequency: 16MHz ;************************************
; Program function:------------------------------ ; Turns on an led connected to PB0 (digital 0) ; when you push a button connected to PD0 ;----------------------------------------------- ; ; PB0 (normally 0V) -----> LED --> 220 Ohm ---> 5V ; ; PD0 (normally 5V) -----> Button ---> GND ;
.nolist .include "./m328Pdef.inc" .list
;============== ; Declarations .def temp =r16 ; designate working register r16 as temp
;================= ; Start of Program rjmp Init ; first line executed
;============ Init: ser temp ; set all bits in temp to 1's. out DDRB,temp ; setting a bit as 1 on the Data Direction I/O ; register for PortB, which is DDRB, sets that ; pin as output, a 0 would set that pin as input ; so here, all PortB pins are outputs (set to 1) ldi temp,0b11111110 ; load the `immediate' number to the temp register ; if it were just ld then the second argument ; would have to be a memory location instead out DDRD,temp ; mv temp to DDRD, result is that PD0 is input ; and the rest are outputs clr temp ; all bits in temp are set to 0's out PortB,temp ; set all the bits (i.e. pins) in PortB to 0V ldi temp,0b00000001 ; load immediate number to temp out PortD,temp ; move temp to PortD. PD0 has a pull up resistor ; (i.e. set to 5V) since it has a 1 in that bit ; the rest are 0V since 0's.
;====================== ; Main body of program: Main: in temp,PinD ; PinD holds the state of PortD, copy this to temp ; if the button is connected to PD0 this will be ; 0 when the button is pushed, 1 otherwise since ; PD0 has a pull up resistor it's normally at 5V out PortB,temp ; sends the 0's and 1's read above to PortB ; this means we want the LED connected to PB0, ; when PD0 is LOW, it sets PB0 to LOW and turn ; on the LED (since the other side of the LED is ; connected to 5V and this will set PB0 to 0V so ; current will flow) rjmp Main ; loops back to the start of Main
Notice that this time we not only have many more comments in our code, but we also have a header section which gives some information about who wrote it, when it was written, what type of controller it was written for, and I have even included a simple circuit diagram to show how you construct the circuit. The rest of the code is also separated into sections.
After you have compiled the above code you should load it onto the microcontroller and see that it works. The LED should turn on while you are pushing the button and then turn off again when you let go. I have shown what it looks like in the picture.
Step 3: Line-by-line Analysis of the Code
I will skip the lines that are merely comments as their purpose is self-evident.
.nolist .include "./m328Pdef.inc" .list
These three lines include the file containing the Register and Bit definitions for the ATmega328P that we are programming. The .nolist command tells the assembler not to include this file in the pushbutton.lst file that it produces when you assemble it. It turns off the listing option. After including the file we turn the listing option back on with the .list command. The reason we do this is because the m328Pdef.inc file is quite long and we don't really need to see it in the list file. Our assembler, avra, doesn't automatically generate a list file and if we would like one we would assemble using the following command:
avra -l pushbutton.lst pushbutton.asm
If you do this it will generate a file called pushbutton.lst and if you examine this file you will find that it shows your program code along with extra information. If you look at the extra information you will see that the lines begin with a C: followed by the relative address in hex of where the code is placed in memory. Essentially it begins at 000000 with the first command and increases from there with each subsequent command. The second column after the relative place in memory is the hex code for the command followed by the hex code for the argument of the command. We will discuss list files further in future tutorials.
.def temp = r16 ; designate working register r16 as temp
In this line we use the assembler directive ".def" to define the variable "temp" as equal to the r16 "working register." We will use register r16 as the one which stores the numbers that we want to copy to various ports and registers (which can't be written to directly).
Exercise 1: Try to copy a binary number directly into a port or special register like DDRB and see what happens when you try to assemble the code.
A register contains a byte (8 bits) of information. Essentially it is usually a collection of SR-Latches each one is a "bit" and contains a 1 or a 0. We may discuss this (and even build one!) later on in this series. You may be wondering what is a "working register" and why we chose r16. We will discuss that in a future tutorial when we dive down into the quagmire of the internals of the chip. For now I want you to understand how to do things like write code and program physical hardware. Then you will have a frame of reference from that experience which will make the memory and register properties of the microcontroller easier to understand. I realize that most introductory textbooks and discussions do this the other way around but I have found that playing a video game for a while first and dicking around to get a global perspective and then reading the instruction manual to get better at it is much easier than reading the manual first.
rjmp Init ; first line executed
This line is a "relative jump" to the label "Init" and is not really necessary here since the next command is already in Init but we include it for future use.
Init: ser temp ; set all bits in temp to 1's.
After the Init label we execute a "set register" command. This sets all of the 8 bits in the register "temp" (which you recall is r16) to 1's. So temp now contains 0b11111111.
out DDRB,temp ; setting a bit as 1 on the Data Direction I/O register ; for PortB, which is DDRB, sets that pin as output ; a 0 would set that pin as input ; so here, all PortB pins are outputs (set to 1)
The register DDRB (Data Direction Register for PortB) tells which pins on PortB (i.e. PB0 through PB7) are designated as input and which are designated as output. Since we have the pin PB0 connected to our LED and the rest not connected to anything we will set all the bits to 1 meaning they are all outputs.
ldi temp,0b11111110 ; load the `immediate' number to the temp register ; if it were just ld then the second argument would ; have to be a memory location
This line loads the binary number 0b11111110 into the temp register.
out DDRD,temp ; mv temp to DDRD, result is that PD0 is input and ; the rest are outputs
Now we set the Data Direction Register for PortD from temp, since temp still contains 0b11111110 we see that PD0 will be designated as an input pin (since there is a 0 in the far right spot) and the rest are designated as outputs since there are 1's in those spots.
clr temp ; all bits in temp are set to 0's out PortB,temp ; set all the bits (i.e. pins) in PortB to 0V
First we "clear" the register temp which means setting all of the bits to zero. Then we copy that to the PortB register which sets 0V on all of those pins. A zero on a PortB bit means that the processor will keep that pin at 0V, a one on a bit will cause that pin to be set to 5V.
Exercise 2: Use a multimeter to check if all of the pins on PortB are actually zero. Is something weird going on with PB1? Any idea why that might be? (similar to Exercise 4 below then follow the code...)
Exercise 3: Remove the above two lines from your code. Does the program still run correctly? Why?
ldi temp,0b00000001 ; load immediate number to temp out PortD,temp ; move temp to PortD. PD0 is at 5V (has a pullup resistor) ; since it has a 1 in that bit the rest are 0V.
Exercise 4: Remove the above two lines from your code. Does the program still run correctly? Why? (This is different from Exercise 3 above. See the pin out diagram. What is the default DDRD setting for PD0? (See page 90 of the data sheet)
First we "load immediate" the number 0b00000001 to temp. The "immediate" part is there since we are loading a straight up number to temp rather than a pointer to a memory location containing the number to load. In that case we would simply use "ld" rather than "ldi". Then we send this number to PortD which sets PD0 to 5V and the rest to 0V.
Now we have set the pins as input or output and we have set up their initial states as either 0V or 5V (LOW or HIGH) and so we now enter our program "loop".
Main: in temp,PinD ; PinD holds the state of PortD, copy this to temp ; if the button is connected to PD0 then this will be ; a 0 when the button is pushed, 1 otherwise since ; PD0 has a pull up resistor it is normally at 5V
The register PinD contains the current state of the PortD pins. For example, if you attached a 5V wire to PD3, then at the next clock cycle (which happens 16 million times per second since we have the microcontroller hooked up to a 16MHz clock signal) the PinD3 bit (from the current state of PD3) would become a 1 instead of a 0. So in this line we copy the current state of the pins to temp.
out PortB,temp ; sends the 0's and 1's read above to PortB ; this means we want the LED connected to PB0, so ; when PD0 is LOW, it will set PB0 to LOW and turn ; on the LED (the other side of the LED is connected ; to 5V and this will set PB0 to 0V so current flows)
Now we send the state of the pins in PinD to the PortB output. Effectively, this means that PD0 will send a 1 to PortD0 unless the button is pressed. In that case since the button is connected to ground that pin will be at 0V and it will send a 0 to PortB0. Now, if you look at the circuit diagram, 0V on PB0 means the LED will glow since the other side of it is at 5V. If we are not pressing the button, so that a 1 is sent to PB0, that would mean we have 5V on PB0 and also 5V on the other side of the LED and so there is no potential difference and no current will flow and so the LED will not glow (in this case it is an LED which is a diode and so current only flows one direction regardless but whatever).
rjmp Main ; loops back to Start
This relative jump loops us back to our Main: label and we check PinD again and so on. Checking every 16 millionth's of a second whether the button is being pushed and setting PB0 accordingly.
Exercise 5: Modify your code so that your LED is connected to PB3 instead of PB0 and see that it works.
Exercise 6: Plug your LED into GND instead of 5V and modify your code accordingly.
Step 4: Conclusion
In this tutorial we have further investigated the assembly language for the ATmega328p and learned how to control an LED with a pushbutton. In particular we learned the following commands:
ser registersets all of the bits of a register to 1's
clr registersets all of the bits of a register to 0's
in register, i/o registercopies the number from an i/o register to a working register
In the next tutorial we will examine the structure of the ATmega328p and the various registers, operations, and resources contained therein.
Before I continue with these tutorials I am going to wait and see the level of interest. If there are a number of people that are actually enjoying learning how to code programs for this microprocessor in assembly language then I will continue and construct more complicated circuits and use more robust code.