Wednesday, August 5, 2009

Programming the ATtiny13 in Assembly

The Arduino introduced me to Atmel's AVR family of microcontrollers, specifically the ATmega168, which led me to purchase an AVRISP MKII ($34 from Mouser) to program off-the-shelf ATmega168s. So when I needed an IC smaller than the ATmega168, it made sense to stick with AVR microcontrollers, rather than invest in another programmer. This is how I ended up writing assembly for the ATtiny13.

I'd programmed a PIC16F877 in college (a long time ago). AVR's flavor of assembly is a bit different from Microchip's, so there were some annoying things to learn when I started programming the ATtiny13. My goal in this post is to familiarize the reader with the AVR-specific assembly grammar.

First, here's how to get set up for burning code onto your AVR microcontroller:

Tools:
A breadboard, a 5V power supply (6V is the max allowable supply voltage for the ATtiny13, 2.2V is the minimum), an ATtiny13, an AVRISP MKII, and a USB cable.

Download:
ATtiny13 datasheet, AVR Studio

Hardware Setup:
Place the ATtiny13 on your breadboard. Refer to page 2 of the datasheet for power/ground connections, and for identifying the four pins that connect to the AVRISP MKII programmer: RESET, MISO, MOSI, SCK. (The ATtiny13 only has 6 I/O pins, and I need to connect four of these pins to the programmer?!? Don't worry: the microcontroller I/O pins that connect to the AVRISP MKII can still be used for other purposes besides programming the microcontroller. In fact, as long as your AVRISP MKII is USB-powered by your computer, you can leave it connected to your microcontroller while you test out your newly burned code. But if you disconnect USB power to your AVRISP MKII, your microcontroller will play dead if the AVRISP MKII is still connected to it -- I learned that the hard way while demonstrating a prototype to investors!)

There's a lot of ways you can physically connect the AVRISP MKII to your ATtiny13. The AVRISP MKII has a 6-position ribbon cable connector (the six connections are RESET, MISO, MOSI, SCK, power, and ground). I started out by just poking solid wires into this connector, and connecting the other ends of the wires to the breadboard. I've improved on this with a neater, more reliable wiring job that I'll detail later.

Ready to burn:
(Assuming you've already installed the AVRISP MKII on your computer) connect the AVRISP MKII to your computer with the USB cable. The LED in the AVRISP MKII might be red or might not light at all. Connect 5V power to the breadboard. The LED changes to green. Start a new assembler project in AVR studio:

Project type: Atmel AVR Assembler (if you download WinAVR, you can select AVR GCC if you want to write your code in C instead -- but what's the fun in that? Actually, AVR microcontroller architecture is supposedly streamlined for C compilers, so the usual argument for programming in assembly -- faster code execution -- doesn't really apply. I guess some of us just feel more comfortable writing in Assembly.)

Project name: TestProj
Check both boxes "Create initial file" and "Create folder."

******
Aside:
The automatically generated "TestProj" folder will hold all the files associated with your project, the two important ones being "TestProj.asm" and "TestProj.hex." A .asm file is the assembly code that you write. A .hex file is the machine-language executable that is burned onto your microcontroller. (The .hex file is a word-for-word translation, converting each assembly language word into a hexadecimal number. All readability is removed, and the order of things is shifted due to conditional branches, but if you have nothing better to do, it is possible to decipher the actual .hex file.)
******

Click "Next" and select "AVR Simulator 2" for the "Debug platform" and "ATtiny13A" for the device (the ATtiny13A is the new and improved version of the ATtiny13, and is the chip you're using if you bought it following the above link).

You should see a bunch of windows. The one in the middle is where you edit your .asm code. I like to begin with a few commented lines:

;TestProj.asm
;Author:
;Date:
;Revision:
/****************************************
What this code does:
1. Light an LED as soon as the MCU has power.

Hardware Setup:
0. One MCU, one LED, and a resistor.
1. Connect LED to pin 5 of the ATtiny13A with a 1k resistor such that the LED lights when pin 5 is high.

To Do:
1. Light the LED in response to an external switch
2. Make the LED glow, Jonathan Ive style
*****************************************/

Cut and paste the above into your .asm file. AVR Studio will color all of the text green, indicating it's all commented out. The above example illustrates two ways to make comments: the ";" for a single line, or "/*" to begin a multi-line comment, ending with "*/". You can also use "//" for single line comments.

Here's the actual code:

.include "tn13Adef.inc"

.def temp = R16

//Pin assignments
.equ LED = PB0 ;pin 5 of ATtiny13

.cseg
.org 0x00
rjmp setup

setup:

//CONFIGURE I/O PINS
ldi temp, (1 << LED)
out PORTB, temp
out DDRB,temp

loop:
rjmp loop

I'll explain this code in a second. First, let's compile it and run it. Cut and paste it into the TestProj.asm file. Assemble it by selecting "Build" from the drop-down Build menu (as with all the other things we're about to do, you can also accomplish this with a keystroke or with a toolbar button; in this case pressing F7, or by pressing the "Assemble" button in the toolbar.)

To see what the code does and make sure it works, we run the debugger: select "Start Debugging" from the Debug drop-down menu. A yellow arrow appears next to the line "rjmp start" (if you're using my initial commented lines, this is line 26 of your .asm file, as indicated by the line and column number at the lower right of the screen). The yellow arrow shows you where the "Program Counter" is pointing. What's the program counter?

There are three kinds of memory in AVR microcontrollers. When you build your code, it's converted into the machine language hex that tells the chip what to do. This sequence of hex numbers is stored in the chip's non-volatile (i.e. turn off the power and the code is still there) "Flash" memory. The "Program Counter" is a counter that starts at 0x00 and counts up, unless some conditional branch tells the program counter to jump to a different value. The value of the counter tells the microcontroller which address to look at in the Flash memory. Picture the Flash memor as an array (a vertical column) of 16-bit words. The top-most word is at address 0x00, the next one down is at address 0x01. The ATtiny13 has 1K Byte of flash memory, organized as a single column of 512 rows, each row 16 bits wide; that means the Program Counter counts from 0 up to 511 (0x000 to 0x1FF -- see Figure 5.1 on page 15 of the ATtiny13 datasheet) because the memory can store 1023 bytes, or 512 16-bit words. Each instruction (a.k.a. "opcode") in your code is stored as one or two 16-bit words in this Flash memory. The opcode is the actual machine language. We'll look at a few opcodes for fun below.

******
A Tangent On AVR Memory:

What are the other two kinds of memory in the ATtiny13?
One kind is non-volatile EEPROM (non-volatile meaning that you don't lose it when power is lost). Unfortunately, there's a relatively small limit to the number of times you can erase/write the EEPROM. The EEPROM memory is not used, unless you make a conscious effort to do so. I've never used it, and I'm not going to mention it again in this post.
The other kind of memory is the volatile "SRAM" (static RAM, it retains its memory indefinitely while the power is on, but forgets everything when the power is turned off). SRAM stores the values of your variables (e.g. a variable that stores the state of a push-button switch). The SRAM, a.k.a the "Data Space" or "Data Memory," is divided into three physically separate parts (see figure 5-2 on page 16 of the ATtiny13 datasheet): General Purpose Registers, I/O Registers, and "internal data SRAM." Although they're all lumped under the same category, they are actually connected very differently inside the MCU, requiring you, the assembly programmer, to know which instructions apply to which parts of Data Memory, resulting in annoying things like different instructions for the same task (setting/clearing bits for instance).

The General Purpose Registers, for example, have direct connections to the ALU. Therefore, most of the assembly instructions are applicable only to these 32 registers. In other words, if you're going to do some operations to a value (add, multiply, manipulate bit values, etc.), the value must first be placed in one of these 32 registers.

The 64 I/O registers have very specific functions, and cannot be used for storing variables, so there's lots of instructions that don't apply to the I/O Registers. Each of the 8 bits in each of the 64 I/O registers has some special meaning. The hardest part of getting started programming in Assembly is learning what bits in what I/O Registers you need to set/clear/check to achieve the desired result (e.g. enable interrupts, use the counter in PWM mode, set an I/O pin as an input, enable pull-up resistors, check the result of a calculation, etc.).

The 64 bytes of "internal data SRAM" is only useful for holding values. If you want to do anything with a value, you need to copy it into the General Purpose Registers. Think of it like this: SRAM is our storage bin of 8-bit words, whereas the registers are the 8-bit words sitting on our workbench. To use anything that resides in "internal data SRAM," we have to put it on our workbench first. Indirect addressing makes this less tedious than it sounds, but more on that later. Besides indirect addressing, the most convenient, and most common way of using "internal data SRAM" is to push and pop values from "the Stack." The Stack is not physically separate from the "internal data SRAM." The Stack is nothing more than SRAM addressed by the Stack Pointer. The Stack Pointer defaults to RAMEND, the highest possible Data Memory Address. To free up register space (there's only 32 after all), a value stored in a register is pushed onto the Stack. Since the "top" of the Stack is sitting at RAMEND, and there's 64 bytes of "internal data SRAM," we can fit a max of 64 values on the Stack (assuming we're not already using some of the "internal data SRAM" for something else).

One last note: it's confusing, but when people say "SRAM" they are only referring to the "internal data SRAM" (in fact, I'm going to drop the phrase "internal data" as well). The General Purpose Registers and I/O Registers are usually referred to as "registers," not as "SRAM."
******

Back to Debugging:
When the MCU runs your code, an internal oscillator is clocking the Program Counter, advancing the pointer (shown graphically in the debugger as a yellow arrow) to the next address in Flash memory. When you are debugging your code, you "clock" the Program Counter manually by pressing F11.

So press F11 once. Note that the Program Counter now reads 0x01 (see the Processor window on the left). That means that the instruction that the yellow arrow is pointing at resides at address 0x01 in Flash memory. Also note that the Cycle Counter reads 2, implying that our "rjmp" command ate up two clock cycles (i.e. if our clock has a frequency of 1MHz, it takes 2 microseconds to execute "rjmp").

Continue to step through your code with F11, and note how the Program Counter changes, and how the Cycle Counter increments. Also note that the yellow arrow never points at the line "setup:" or the line "loop:". These are labels, not instructions. They do not reside in Flash memory. Labels allow us to assign a name to a location in our code, so that we can easily tell the code to jump there. This is more readable than "rjmp 0x01," and it is more flexible, allowing us to insert code before the label, without having to recalculate what Flash address the label is referring to.

When you get up to the line "rjmp loop," the Program Counter is stuck at Flash memory address 0x05, and the Cycle Counter continues incrementing by two for each execution of the rjmp command. The ATtiny13 will remain stuck in this loop forever. Let's stop debugging for now, and burn the code onto ATtiny13. Select "Stop Debugging" from the Debug drop-down menu.

Burn the code onto the ATtiny13 by clicking on the "Con" button on the toolbar (the button looks like an IC with the name Con). This opens up a dialog box to establish a connection between the AVRISP MKII and your computer. Now click on the "AVR" button (to the right of the "Con" button). Go to the "Program" tab, and in the section labeled "Flash," check "Input Hex File" and browse to the .hex file you just built (it's in the TestProj folder). Click the "Program" button to burn the code onto the chip. The actual code burning takes a fraction of a second (which feels impossibly quick if you're used to uploading code to the Arduino). Your breadboarded LED should light up, almost the instant you press the "Program" button.

Some More Debugging:
Run the debugger again. This time, as we step through the code, let's look at what's happening in SRAM. In the Processor window, expand the "Registers." We define "temp" as register 16, so keep an eye on register 16 when you step through the line "ldi temp,(1 << LED)." LDI (stands for Load Immediate) is an instruction with two operands. The first operand is the General Purpose Register where you wish to store a value. Since we defined "temp" to be register 16, we're going to store some value at register 16. When we stepped through this line of code, we saw the value stored was "0x01." How is "1 << LED" = 1? As we'll see later, the .equ statement sets LED = PB0, which is set = 0 in a .equ statement in the .include file. So 1 << LED is translated to 1 << 0. The operator “<<” means bit shift to the left. “1 << n” means bitshift 1 n times to the left. For example, 1 << 0 = 1, 1 << 1 = 2, 1 << 2 = 4, 1 << 3 = 8, etc. So if we want to store the value “1” in temp, why didn't we simply write:

ldi temp, 1

Believe it or not, “1 << LED” is infinitely more readable than “1.” Say I want to connect my LED to PB2 (pin 7) instead of PB0 (pin 5). I only need change my .equ directive from .equ LED = PB0 to .equ LED = PB2.

The next line outputs the value in "temp" onto PORTB. In the I/O View window, expand "PORTB". In the lower part of the I/O View window, watch the LSB in register PORTB get shaded in. Similarly, the LSB of register DDRB is shaded in as you step through the next line of code. Independent of what code is executed next, the LSB of register PINB is shaded in after the next clock cycle.

This is the essence of debugging. Step through the code line by line, and satisfy yourself that the correct bits are being set and cleared in the correct registers.

A Full Explanation of the Code:


.include "tn13Adef.inc"
/*
Include the file "tn13Adef.inc." This file is mostly just a compilation of .equ directives. Like labels, directives are not instructions stored in Flash memory. They are interpreted by the assembler when you compile the code. ".include" is a directive. Here's another directive:
*/

.def temp = R16
/*
We're assigning the name "temp" (for temporary variable) to register 16, just to make the subsequent code more readable.
*/

.equ LED = PB0
/*
Everywhere the word "LED" appears in our code, the compiler replaces it with the value 0 (because of the statement ".equ PB0 = 0" in the .include file).
*/

.cseg
/*
Yet another directive. Recall we said that there's three kinds of memory: Flash, SRAM, and EEPROM. Well, CSEG (stands for Code Segment) defines the start of a code segment, and code, as we know, resides in Flash (Program memory). To explicitly store stuff in SRAM, we'd use a ".dseg" directive (stands for Data Segment). More on that later.
*/

.org 0x00
/*
This directive tells the location counter that the next line of code belongs at the address indicated. Since our .org statement appears after ".cseg", we're in the code segment, which means that our location counter is the Program Counter, and the address is a location in Flash memory. And 0x00 means that the next line of code is to be stored at Flash address 0x00.
*/

setup:
/*
This is a label. It's just a way for us to name a location in memory, making the code more readable, and making it easy to program conditional jumps to this location in memory. Since the "setup" label is appearing in a Code Segment (.cseg), the label refers to a location in Program Memory. If the label were part of a Data Segment(.dseg), then it would refer to a location in SRAM (that's how you create variables).
*/

ldi temp, (1 << LED)
out PORTB, temp
out DDRB, temp
/*
We covered LDI above. OUT, as the name suggests, takes the value of "temp" and outputs it to I/O Registers PORTB and DDRB. This is a good example of the annoyance of AVR's Data Memory. When copying values from one General Purpose Register to another, you use the MOV instruction. But to copy a value from an I/O Register to a General Purpose Register, you use the IN instruction, and to go the other direction, you use OUT. Examples:

mov r16, r17 ;copy r17 to r16
out PORTB, r17 ;copy r17 to PORTB
in r17, PORTB ;copy PORTB to r17

The statements:
mov PORTB, r17
out r16, r17
are nonsensical and will generate errors when you compile.
*/

loop:
/* Another example of a label. This label refers to Program Memory address 0x05. Obviously, if we decide we need another line of code in setup, then "loop" refers to memory address 0x06. You get the idea.
*/

rjmp loop
/*
Go to loop (go to memory address 0x05). And what's at memory address 0x05? A instruction to go to memory address 0x05. The ATtiny13 spins its wheels indefinitely. There's more elegant ways to make the ATtiny13 stop doing stuff. And soon we'll be adding code to the loop routine.
*/

Since .cseg and .org 0x00 are defaults for the compiler, you could erase the ".cseg" and ".org 0x00" from your code, though it won't make your .hex file any smaller. Anyway, it's a good idea to get used to these directives for when your code starts using more memory. For example, here's a code snippet that defines a location in SRAM:

.dseg
;DSEG = Data Segment, so the code that follows
;applies to Data Memory (SRAM)

.org 0x60
;The next line of code shall reside at SRAM address 0x60.
;0x60 is the lowest address we can use for SRAM,
;(the registers reside below 0x60).

myData:
;myData is the label for this Data Memory address.

.BYTE 1
;reserve 1 byte at the location labeled "myData."

Machine Language:
What about the opcode machine language stuff. How does the "rjmp loop" opcode actually look in Flash memory? The opcode for the rjmp instruction is "1 1 0 0 k k k k k k k k k k k k" where 1100 is the code for rjmp, and the 12 k's are the 12 bits of the address to jump to. (Of course, only nine of these 12 bits are necessary to address the 512 rows of 16-bit wide Flash memory.) So, for example, the opcode for "rjmp setup" is:

1100 0000 0000 0001

because the label "setup" refers to Program memory address 0x01. How do we know that "setup" labels address 0x01? the .org 0x00 statement sets the Flash address to 0x00, so the "rjmp setup" command resides here. The very next line of code, then, resides at address 0x01, which is where the "rjmp setup" jumps us to. (So why'd we even bother with the "rjmp setup"? Why not just begin the code with the "ldi" instruction? This question will be answered in our next program, where we'll see that the first seven or so lines of Flash memory have a very special purpose).

Similarly, "rjmp loop" is:

1100 0000 0000 0101

because the label "loop" refers to Flash memory address 0x05. Again, just count lines of actual code, and you'll see that the line "rjmp loop" belongs at address 0x05, (as the Program Counter indicated while debugging).

Let's take a look at this in the actual .hex file. Expand the "Output" folder in the file tree at the left, and double-click "TestProj.hex". Here's what's in my .hex file:

:020000020000FC
:0C00000000C001E008BB01E007BBFFCF1F
:00000001FF

1100 (rjmp) is represented in hexadecimal as C. Ignore the first line of the .hex file. Can you figure out where the "rjmp setup" instruction is? Remeber that every four bits of opcode is represented by a single hex number (1100 = C). You can obtain the opcodes (and much more useful information) from the Assembler help file, availble in the Help drop-down menu. You're looking for 1100 0000 0000 0001, which is C001. This appears in the middle of the second line. But we don't have to dig this way. Go back into debug mode. From the View drop-down menu, select "Memory." A window pops up that allows you to view any part of the ATtiny13 memory. Select the "Program" memory. You can directly read the four hex numbers sitting at each 16-bit memory address. This is where I throw my hands up -- the numbers seem a bit jumbled compared to the opcode formats described in the help file.

Next post, we'll introduce interrupts to make the promised code revisions in our "To Do" list.

4 comments:

Nerdful Things said...

How much cheaper is a Atiny compared to the Atmega 168?

My microcontroller projects are on
www.Nerdful.com (n00b)
.

SustainableLab said...

Prices from Mouser (qty of 1):
ATmega168: $4.08
ATtiny13: $1.40
ATtiny24: $1.94

You might find them cheaper from Digikey or Newark. But I usually buy from Mouser: easier search engine, and better on-line tools for projects. Too bad Mouser doesn't carry National Semiconductor components.

SustainableLab said...

Just discovered this great electronics component search engine:

http://www.findchips.com/
(with a part number like ATtiny13, you'll get lots of hits for all the different flavors of the chip, so you'll need a more exact part number, which you'll find in the "Ordering Information" section of the data sheet, e.g. SOIC-8 package vs. DIP-8 package.)

Much thanks to David L. Jones and his video blog:
http://www.alternatezone.com/eevblog/?m=200904

Bartingale said...

Ey thank's a lot for this nice lesson.