Welcome to Tutorial 10!
Sorry it took so long to get this next installment out but this is a pretty hectic time of the year. In any case, here it is!
We have come a long way and you are probably already proficient enough to write many interesting programs and control amazing devices. I have to say I am impressed at how many of you are following these tutorials. I sort of thought that I would do the first couple and then fold it due to lack of interest but now it is 10 tutorials later and I still have a few of you "Liking" them to show me that you are still interested.
There are a number of things we have yet to cover. These are communication among multiple controllers and also use of the EEPROM among a few other things. These will come in to our discussion naturally as we finish the project that we are working toward here.
Today we will first move our 4-digit display to an external prototyping board in a similar fashion to what we did with the dice-roller in Tutorial 8, then we will set up communications between our main microcontroller and these two periferal devices so that we can design the first part of our final project!
For this Tutorial you will need:
- 4-digit display prototyping PCV boards (same as Tutorial 8): http://www.ebay.com/itm/171561943265
- Microcontrollers, xtals, caps, and reset buttons for the 4-digit display.
- long female headers (also called "stackable headers") for the external PCB modules:
Here is a link to the complete collection of my AVR assembler tutorials: https://www.instructables.com/id/Command-Line-AVR-T...
Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.
Step 1: External Prototyping Boards for Our Display
I have attached the pinmaps for the 3 possible breakout boards that we could use for our displays. I have found that the first one is difficult since they have already laid out a pattern that they expect us to use and it doesn't work, so we would still have to attach various wires in order to get the single 7-digit display to work. So I won't be using that one. The second one is also a Measure Explorer and it is larger that the one I used before, however, it only has connections on one side of the board and I find it pretty flimsy (if you touch it too long with the iron the tin/copper comes away and screws up the hole). The last two are different perspectives on the one that I used for the dice roller and I like it the best since it has connections on both sides (to help during the many times I screw up my cutting or my soldering), the metal around the holes doesn't come away as easily, and it also has pre-drilled holes for attaching the final product to the container I will be putting them all in. So that is the one I am going to try to use all the time unless I am completely forced by size constraints to change.
Step 2: Build the Breakout Board and Solder the Components
I have given a series of photos that show the board map that I cobbled together (you may find a better one) and the front and back of the board after I used my dremel to cut out the circuit. The last two pictures show the front and back of the finished board. It has header pins so that it can be powered, programmed, and communicated with. Also note that I took out the 330 ohm resistors that go to the 7-segment displays and replaced them with 100 ohm resistors since I wanted a bit brighter display.
We will test the display in the next step, but you should note that it is always best to test your code on a breadboard setup before you solder things to a breakout board. That way when things don't work properly you can be sure that it is your wiring job and not your code that is at fault.
Step 3: Testing the Display
Now that we have moved our 4-digit display to its own board we need to test it to make sure it works (and to fix the inevitable problems). This means that we want to write a simple driver that will just display some number and check all of the segments on each digit. Once we are confident that everything works as it should then we can work on our communications code.
The way we will write the test code is so that we can continue to use it later by simply adding the communications routines. So we copy our 4-digit-display code from the last tutorial and remove all of the stuff that had to do with the ADC since we won't be using that anymore. We also changed the anode pins so that they use PC0 through PC3 since it makes wiring and coding easier.
I have attached the code and a picture of the operation of the external display.
I don't need to go through all the code line-by-line since it is mostly just simple modifications of the things we have already done. However, there is one thing I should note.
We are eventually going to need to display numbers from 0000 up to 9999 on our 4-digit display. This means that we need room for 10,000 numbers. However, a 8 bit register only goes up to 255. So we are going to need more bits devoted to the number we want to display. Let's do a quick calculation to figure out how many bits we are going to need. Each digit is a power of 2 and so we want to see what power of two gives us 10,000. For this we use the "natural logarithm":
2^x = 10000
x ln(2) = 4 ln(10)
x = 4 ln(10)/ln(2) = 13.288
This tells me that in order to display 10000 we need more than 13 bits. So if I use 14 bits then I will have more than enough. As a check (or just do this in the first place if you don't think math is fun), just go to the "math is fun" website automatic converter here:
and figure it out by simply converting. You will find that 14 digits can display numbers up to 16383 and 13 digits only up to 8191. So our calculation is correct, we need more than 13 digits and less than 14. So we use 14. In fact, 9999 in binary is: 0b10011100001111 which is 14 bits, but doesn't need all of them on.
Okay, so the way we solve the dilemma of needing more than 1 register is to use 2 of them. This gives us 16 bits and is therefore plenty to display any number we throw at the 4-digit display.
But when we send the numbers across our communications interface they will be in binary. How do we convert a binary number into decimal digits to display? This is not nearly as easy as you might think at first glance. Let's figure it out.
We start with a number between 0 and 9999 and we want the 4 digits of that number so we can display them. There is no function in assembly language that will do that for us so we have to make one ourselves. The number begins as a 2-byte binary number. Lets call it X for now. We would like to figure out each digit in turn from the highest to the lowest. The way we find the highest is as in this code outline:
digit = 0
X > 1000?
X = X - 1000
As you can see, this looks quite simple. We just take off 1000's one at a time and each time we increase the value of "digit". Once X is no longer bigger than 1000 we will have the number of 1000's stored in "digit" and we return it. This will give us the first number for our display. Then we can do the same thing as above to the remaining value of X except with 100's instead of 1000's and this will get us the number of 100's, then we do it again with 10's, and finally when there are no 10's left, the amount remaining in X will be between 0 and 9, and thus it will be our last digit. So that is how we can get our 4 digits.
However! It is not nearly so simple in assembly language! A huge wrench is thrown into this plan because of the fact that we need two bytes to represent our 14 digit binary number. How do we subtract 1000? In 2-byte words, 1000 is represented by 0b0000001111101000 and you see that some of it is in the first byte and some of it is in the second byte. So instead of X = X-1000 we could write X as the two bytes XH and XL so that X = XH:XL and then do our subtraction as follows:
XH = XH - high(1000)
XL = XL - low(1000)
That seems simple enough right? Well it gets even worse! Suppose we started with X = 2001 and we find our first digit (the 2) by subtracting 1000's. Let's see how it would work:
We test X and find that it is > 1000 and so we subtract, since 2001 = 0b0000011111010001 in binary our subtraction would work as follows (putting the numbers in):
XH = high(0b0000011111010001) - high(0b0000001111101000)
XL = low(0b0000011111010001) - low(0b0000001111101000)
XH = 0b00000111 - 0b00000011 = 7 - 3 = 4
XL = 0b11010001 - 0b11101000 = 209 - 232 = -23
We have a problem! The low byte does not contain enough to handle the subtraction! You might think this isn't a problem since assembly language can handle negatives, but it can only handle up to -128 and there will definitely be times when the result of the XL subtraction is more negative than that. So what do we do?
Well, the way we deal with this is the same way we do it in any subtraction from the 5th grade. We need to take one of the digits from XH. Remember that each binary digit in XH is actually equal to 256. We do this with the "sbc" command which subtracts with carry.
Then, once we have gotten rid of the 1000's we move on to the 100's and do the same thing. In that case we have the same problem in that we will need to borrow 1's from the high byte until our result is less than 256 so we continue to use the sbc to subract the high byte.
Finally, when we get to the 10's and the 1's spots we no longer have to worry about borrowing digits anymore and things are simpler, we can just subtract from the low register.
So now that you see how we are forced to do things take a look at the code and you should now be able to figure out how it works. Notice that there is some things we needed to do to display numbers between 768 and 999. Can you think of why that may be? In any case, we begin by simply giving it a number to display so we can test our display. We will simply use 9876. Later we would like the number to come from another microcontroller via the two-wire communications interface.
Exercise 1: Figure out how the code fragment below works.
Here is the explicit code I use for converting the two byte binary to decimal for the display digits:
loadCash: ; this computes the digit for each 7-seg of the display<br>cli ; disable interrupts while loading digits<br>push playercashH ; store high byte of playercash<br>push playercashL ; store low byte of playercash<br>ldi tempH,high(1000) ; set tempH:L to 1000<br>ldi tempL,low(1000) <br>rcall computeDigit ; find the 3rd digit<br>mov digit3,digit ; store the new 3rd digit<br>ldi tempH,0 ; set tempH:L to 100<br>ldi tempL,100 <br>rcall computeDigit ; find the 2nd digit<br>mov digit2,digit ; store the new 2nd digit<br>ldi tempH,0 ; set tempH:L to 10<br>ldi tempL,10 <br>rcall computeDigit ; find the 1st digit<br>mov digit1,digit ; store the new 1st digit<br>mov digit0,playercashL ; remaining is 0th digit<br>pop playercashL ; restore low byte of playercash<br>pop playercashH ; restore high byte of playercash<br>sei ; re-enable interrupts<br>ret
computeDigit: ; interrupts are already disabled while here<br> clr digit compareHigh: cpi playercashH,0 ; if both playercashH brne PC+3 cpi playercashL,0 ; and playercashL are zero then done breq returnDigit cp playercashH,tempH ; otherwise continue brlo returnDigit ; if less then move to next power of 10 brne PC+3 ; if high is greater then subtract cp playercashL,tempL ; if highs are equal and lows are less, brlo returnDigit ; then playercash is less so move to next power of 10 clc sub playercashL,tempL ; otherwise subtract power of 10 sbc playercashH,tempH ; otherwise subtract power of 10 inc digit ; and increment the current digit rjmp compareHigh ; and go back up to test again returnDigit: ; digit will now be the one to display in this spot ret
Next we will introduce something new -- TWI communications.
Step 4: TWI Communications Overview
In this tutorial we would like to set up communications between our dice-roller and our 4-digit display so that the result from the dice roll appears on the display. We could very easily just use the "roll your own" method of communicating that we did before with our "register analyzer" and the dice roller, however, in future tutorials we will be adding more components and we would like all of them to be able to communicate with one another. So it is much better if we use a protocol that only uses two wires and can still communicate with multiple slave controllers. That is exactly what the TWI built in to the AVR microcontroller does. So we will now learn how to use it.
You will want to turn to Chapter 22, page 206, in the ATmega328P datasheet where it discusses the TWI. It would be a good idea to peruse it and get a general overview of how it works. We will be using the easy method of getting started which is to look at the sample code they give on page 216 and modifying it for our purposes. At the moment we are only communicating between two microcontrollers and so this code is pretty much all we need. Later we will need to expand on it but we will cover that extra material when we actually need it.
Here is the code fragment that they give in the datasheet:
ldi r16, (1--TWINT)|(1--TWS)|(1--TWEN)
Note that in the above code statements like
are actually supposed to have "<" in them instead of the "-" signs but for some reason the stupid editor that Instructables has chosen to give us likes to make assumptions about what we are writing and it will delete everything after a "<" when you try to publish it. This is because it assumes that when you are writing "code" it is html and everything after a "<" is a link or instruction not to be displayed. It also adds the
line break symbols back in at random times so that I get everything right, and the next time I log on, I see that the code has become all one long line with "
"'s where the line breaks should be! Argh. Unfortunately, if I try to write our code in some other type, like "quote" or "list" or "normal text" it will make assumptions on the spacing and formatting and it won't allow me to have the lines of code on different lines without double spacing between them and indenting. All of which ruin the look of the code and make it hard for you to read properly. So I use the "code" format intended for html and just work around the problem parts. What they really need is a type similar to the LaTeX one called "verbatim" where it will just print out verbatim what you type and not try to dick with it. Anwyay, you are probably starting to see some of the reasons why I like command line editors (like vi) and command line coding better than IDE's and HTML/Java based editors. Also, one more annoyance is that if you write things in another editor and then copy/paste into this one it removes all of your formatting and pastes it all as one long line that you then have to go through and reformat! This editor is tedious and annoying. Writing these tutorials has been an exercise in patience and tolerance, let me say, and by the time we are finished if I have not been driven to alcoholism and prescription drugs, I am sure I will be qualified for a life as a Benedictine or Bhuddist monk.
Whew! Okay! Let's get to work.
If you examine the above code fragment you see that it is separated into several blocks by wait1, wait2 and wait3 labeled loops. Inside each of these we are waiting for the TWINT flag to be set to indicate that the TWI has finished whatever we last told it to do. Between these wait blocks is the code to start communications, to transmit the slave address of which microcontroller we want to talk with, to transmit the data to that slave, and finally to stop the transmission. This results in a single byte of data being transmitted.
Before we continue with the transmission code fragment, you should grab your copy of the m328Pdef.inc include file and go to the section dealing with TWI. You will see the definitions of the registers associated with the TWI and the names of the various bits in these registers. In the code fragment above, as usual, we use the names of the bits and the left shift operation rather than the bit number so that the code is easier to read. The assembler doesn't care though. As I have mentioned several times in the past, if you wanted to write
it would work perfectly well, in fact, the assembler sees the first one, but you would have to be Rain-man to understand your code six months from now and you would have no computer friends because everyone trying to read your code would hate you. I only mention it again to remind you of the distinction between what is part of the assembly language and what are just labels we have written ourselves to help us understand our code. That is all the m328Pdef.inc include file is. If you wanted to go through the include file and change the names of everything you could do that as well. You could call TWINT "beavis" and TWEN "butthead" if you wanted, but then you would have trouble reading the datasheet. In fact, all that `Buddy', the guy (or computer) that wrote the include file did, essentially, is just went through the datasheet and looked at all of the tables defining the registers (like the one on page 217 defining the TWCR register) and wrote the .equ statements for them and their bits. So if you were writing code for a new microcontroller and you didn't have the include file you could simply use the datasheet and define the bits and registers that you use in your code at the top of your program with a few .equ statements. Anyway, lets move on...
There are a few things in the code fragment above that are not in the include file. For example START, DATA, and ERROR. DATA is the register that you want to send. ERROR is a subroutine that you may want to write to catch problems with the transmission and deal with them. START is the condition in the TWI status register (TWSR) that you want to compare with. This changes according to what you are doing. For example, if you are the master microcontroller in Transmitter mode then table 22-2 gives the Status codes that you can choose from. Similarly tables 22-3, 22-4, and 22-5 give the status codes for the Master Receiver, Slave Transmitter, and Slave Receiver modes respectively. So depending on which microcontroller in your network your code is assembled for you will set START accordingly.
So what you should really do if you want to understand this stuff is to go through the code fragment line-by-line, looking up the registers in the datasheet, and figure out exactly what is happening at each step. We will write our own version of the above code fragment in our dice-roller and our 4-digit display code. In this tutorial our dice roller will be the master and the 4-digit display will be the slave. However, in the future there will be more slaves and there will be more masters including one that runs the show and communicates with them all. We are going to go through the detailed operation of the TWI as it reads each line of our code so that we know exactly what is happening on the line, and why we need to wire the lines and execute the commands we do. However, first we need to look at how to connect the two microcontrollers together and what exactly is going on across each of the TWI lines.
Exercise 2: Look up the register memory locations in the datasheet for the TW Control Register (TWCR), the TW Status Register (TWSR), and the TW Data Register (TWDR). Then examine the sample code given above which is copied directly from the datasheet. Can you find a glaring error which makes the code inoperable on the ATmega328P? Can you figure out the changes needed to fix it?
Step 5: Wiring the TWI
We are going to implement the TWI as an interrupt subroutine in the slave (the 4-digit display) and as a normal subroutine in the master (the dice-roller). This way everytime there is a roll of the dice (i.e. when you push the button), the dice roller will communicate the result to the display which will display the result in the last two digits of the display.
So, for example, a roll of double sixes would result in the transmission of the number 12 to the display and this would be parsed by the routine we wrote earlier in this tutorial and displayed as a 1 and a 2 on the last two 7-segment displays. Notice that we don't even need the "high" register that caused us so much trouble in the binary to decimal conversion code. As you have probably figured out by now, our eventual use for the 4-digit display is not simply to display the dice rolls. We are doing that for now because it is a simple way to get the communications code going without adding too much new stuff at once.
So let's start by analyzing how the TWI works. We will start with our Dice roller and add a couple of lines of code just to get things going. In both the display and in the dice roller we need to put the SDA and the SCL pins to INPUT and high. I.e.
cbi DDRC,4 <br>sbi PORTC,4<br>cbi DDRC,5<br>sbi PORTC,5
Now, in the INIT section of the code let's add the following
lds temp,PRR<br>andi temp,0b01011111<br>sts PRR,temp
This sets the PRTWI bit and the BRTIM0 bits of the Power Reduction Register to 0. See page 45 of the datasheet. This makes sure both the TWI module and the Timer/Counter0 module are awake. We need the Timer/Counter0 for our "delay" commands that we used elsewhere in the code. The TWI one is the one we are concentrating on at the moment.
Now we want to set the SCL (Serial CLock line) frequency. We do this using the formula given on page 213. Now, for our present purposes, I am going to set the frequency as LOW as I possibly can so that we can analyze the signal a bit easier. I may then set it higher again later but for now lets make it small. Here is the formula:
SCL freq = CPU/[16 + 2*TWBR*Prescaler]
If I want this as low as possible, I should set TWBR and Prescalar as high as possible. The TWI Bit Rate register, TWBR, is an 8-bit register and so the biggest value I can make it is 255 (which is all 1's, i.e. TWBR = 0b11111111), also the prescaler bits are defined in table 22-7 on page 232. These are bit 0 and bit 1 of the TWSR (Two Wire interface Status Register). The table tells us that if we want the highest prescaler I should choose TWPS1 = 1 and TWPS0 = 1 and this will give us a prescaler factor of 64. Hence my Serial Clock frequency for my TWI line, using the formula above, will be
16 MHz/(16 + 2*255*64) = 16000000Hz/32656 = 489.9 Hz or about 490 Hz.
So we will do this in our INIT part of the code with the lines:
ldi temp,255<br>sts TWBR,temp<br>ldi temp,0b00000011<br>sts TWSR,temp
Now when we start the TWI our SCL clock frequency will by 490 cycles per second (490 Hz). We now add the following lines to start up the TWI as given on page 217 (i.e. Master Transmitter Mode):
ldi temp, (1--TWINT)|(1--TWSTA)|(1--TWEN) <br>sts TWCR,temp
and remember that the - signs in the above are supposed to be "less than" signs but the stupid Instructables editor doesn't like them.
This sets 3 of the bits in the Two Wire Control Register (TWCR) and you can read about these bits (toggle switches) in the description of that register on pages 230 and 231. Essentially, the TWINT bit set to 1 clears the two wire interrupt flag, the TWSTA bit set to one defines the dice roller as the Master on the line and sends a start condition signal down the two wire interface line, finally, the TWEN bit set to one enables the TWI and activates the interface.
That is all for now! Just put those lines in the INIT part of the code, assemble it, and let's see what happens.
We want to hook things up so that our TWI wires go from our diceroller to our display. We don't need to write anything for the display yet except that we want the SDA and the SDC pins to be INPUT and 5V on both the dice roller and the display. Now if we turn on our diceroller and our display and hook up an oscilloscope between our SCL line and GND we will see the scope trace shown in the first two pictures. You can see that it is 490 Hz just like we wanted it to be. [see pictures 1 and 2]
Now, if you have read the section of the datasheet about the TWI you will notice that they say that you need pull up resistors to 5V on each of the SDA and SCL lines. Why is this? We just looked at our signal and it looks like a very nice 490 Hz square wave without anything other than our internal pullup resistors on each of the pins! That is exactly the kind of signal we want for communications (as we will discuss more in a bit).
What would pullup resistors on the SDA and SCL lines do? Well, they simply make the signal go back to 5V faster. If I were to pull it down to 0V and then let go, it would spring back to 5V much faster with these extra external pullup resistors than with just the internal ones on the pins. However, at the frequency of 490Hz it doesn't make any difference. The signal is just fine without them. In fact, we will be using 490Hz as our TWI frequency because our application doesn't need to go any faster. I don't think anyone is going to care if you can only send 490 bits of data per second. If we had an application that needed high speed data transfer we would increase our clock speed and then we might have to add external pull up resistors.
Let me now show you why you may someday want to put pullup resistors on the lines. Let's increase our SCL frequency. Go back to the lines in the INIT section of the dice roller code (the Master sets the speed of the line not the slave) and change it so that our frequency is the highest possible this time instead of the lowest. This means in our formula
SCL freq = CPU/[16 + 2*TWBR*Prescaler]
we want the denominator to be as small as possible. So lets just leave the prescaler alone and set TWBR = 0. Then our frequency will be 16MHz/16 which is 1MHz (note that the CPU will still be 16 times bigger than the SCL).
Now assemble it, run it, and examine the oscilloscope trace (I use a DSO Nano oscilloscope from Seeedstudio.com which is a very inexpensive oscilloscope that comes in handy with this kind of stuff). [See picture 3]
Now take a look at the new trace. You notice right away that at this high frequency, the SCL line doesn't even have time to get up to 5V before it is sent back to 0 again! So we don't get a nice square wave. We need a lower frequency so that at least we oscillate between 0V and 5V. If we now turn to page 308 we see that Table 29-14 gives the properties and limitations of the TWI. It says the that SCL clock frequency should be a maximum of 400kHz. No wonder our trace sucked so bad. So lets set TWBR to 12, and turn off the prescaler bits. This will give SCL freq = 16000/(16+24) = 400kHz. Then assemble, upload, and take a look at the new trace.
I won't attach a picture. Let's just say we still get a crappy trace that doesn't quite make it to the top. We could add some resistors to the line at this point, but let's make the frequency just a bit lower so that you can really see what is going on.
Lets set TWBR to 72. This will give us a frequency of 100kHz. Take a look at the picture of the trace. [See pictures 4 and 5]
Now you see that the signal is oscillating between 0V and 5V but it doesn't get back up to 5V very quickly! In fact, it takes so long to get back up to 5V that by the time it gets there it is already time to go back down to 0V again. This is not what we want if we want to transmit data.
Take a look at Figure 22-2 on page 207. You will see how the data is transmitted. The CPU essentially compares the signals on the two lines (SDA and SCL) and it decides what the data is by how they compare. This is why the CPU clock has to be at least 16 times higher frequency than the SCL clock. The CPU needs those extra cycles to distinguish what is going on with the SCL and SDA signals and make decisions based on them. It needs two types of regions as shown in the diagram. It needs regions where the data is "stable" meaning it is steady at either 5V or at 0V for a certain period of time. It also needs regions where the signal is changing, either going from 0V up to 5V or from 5V down to 0V. We will discuss how it gets information out of this in a second, but my point is that in the 100kHz signal that we just looked at, we don't have a long enough "stable" region. We essentially have nothing but transitions. So it won't work. To fix this we add pullup resistors to the two lines. This will cause the signal to get back up to 5V faster and give us a longer stable region at the top. Let's try it. Grab two resistors. I am going to use 4.7kOhm resistors. Then wire them into the lines as in the diagram on page 206. I will attach a picture of my wiring job. [see picture 6]
Now look at the trace! We have the signal jumping back up to 5V and then remaining flat for awhile before fall quickly back to 0V. In other words, we have a square wave trace that the CPU can use to send data. [see picture 7 and picture 8]
So that is why they suggest you add pullup resistors to the lines. In fact, if you want to figure out what size resistor you need you could either figure out the line capacitance and use the formula they give in Table 29-14 or you could simply look at the oscilloscope trace and swap resistors until you get a nice square wave.
Anyway! We are going to go back to 490 Hz We put our TWBR back to 255 and our prescaler back to 64 and then we can get rid of the pullup resistors altogether and just hook one microcontroller to the other. The internal pullups do fine for pulling up the 490 Hz signal fast enough to get a nice square wave as we already showed in the photos. Look at the last picture [picture 9] showing the 490 Hz wave with the pullup resistors and compare it with the first two pictures which show the same 490 Hz wave without the pullup resistors. See any difference? Me neither. So we will dispense with the resistors and just hook our dice roller directly to our 4-digit display.
Now that we know how the SDA and SCL lines work (i.e. using square wave signals) we will now look at how data is transmitted using them.
Step 6: How Does the TWI Transmit Data?
We would now like to discuss exactly what is happening when the TWI is transmitting data.
The first thing we transmit is the "start condition". This is a falling SDA signal when the SCL signal is high. So if you are looking at the two lines you will see the SCL going up and down between 0V and 5V in a square wave 490 times a second. The SDA line, on the other hand, just sits there at 5V. Now, all of a sudden, the SDA line drops down to 0V at the same time the SCL line is in the high (5V) part of its cycle. This is a "start" condition. When the slave see's that happen it springs into action.Well... I say "springs into action" but since the slave CPU frequency is 16 MHz and the SCL frequency is 490 Hz it means that the slave has 32653 cycles to sit around twiddling its thumbs before the next SCL cycle comes along. So it is hardly "springing into action" but you get my meaning.
When the slave detects a start condition, it watches the lines and starts recording on the next high clock pulse. The next 7 bits will be the address of the slave that the master wants to talk to. So, for example, suppose I am the slave and I see SDA drop to 0V during a high pulse on the SCL line. Then I know transmission has started, I then see the SCL go to its low part, then back up to high again, then I look at SDA during this high part and see it is low (0V), this means the first bit of the address is 0, I then wait for the next high pulse and again look at SDA, again I see a 0V, so the next address bit is another 0, then the next pulse I see 5V on the SDA line, so I know the next address bit is a 1, and this goes on until I have the 7 address bits. Once I have the entire 7 bit slave address I compare it with my own address and if it is different I just ignore the TWI line after that. If it is the same as my address then I know the master controller wants to talk to me.
I then wait for the very next high SCL pulse and if SDA is a 1 then I know the master wants to Read from me. This means the master is in "Master Receiver Mode". Otherwise if SDA was a 0 instead it would mean master wants to Write to me. This means the master is in "Master Transmitter Mode". Now I know if I am the slave being address, and I know whether it is a read or a write. This took 8 clock cycles and transmitted 8 bits (7 address bits and a read/write bit) which is good because I am automatically storing that byte in a data register called TWDR which I will use to figure out if it is my address and if it is a read or a write.
Then I have to acknowledge that I received that information so I pull the SDA line low in the next high SCL cycle (the 9th cycle since the start) and this tells the master than I got everything and I am ready for the next data packet.
If I want, at this point I can pull the SCL line low and just keep it there as long as I want. This will "pause" things until I let it go high again.
Once things continue, suppose it was a Write signal. That means the master will now send me some data bytes. So I watch the SDA line again and collect the bits as they come in. This time there will be 8 bits (going from MSB to LSB, i.e. from bit 7 to bit 0) and then, if I got them all without any problems, I will bring the SDA line to 0V again in the 9th cycle to acknowledge that I got them. This can continue indefinitely, sending byte after byte. Each time I get one it will be automatically stored in TWDR and it is my job to copy it out of there and do something with it before the next data byte comes in and writes over it. Finally, when the master is finished sending me data, a "stop" condition will be sent. This is when the master pulls the SDA line high in the middle of the high part of the SCL clock cycle. When I see this I am finished with the TWI until I detect a start condition followed by my address again.
Notice something. When the SCL clock cycle is LOW is when the controller is setting up the next bit. For example if I want to send a 1 to the slave in the next clock cycle, I use the time when the SCL line is low between cycles to set the SDA line either high or low so that when the next high cycle appears my SDA line is already portraying the bit I want to send. Then I leave it either 0 or 1 through that entire high phase until the SCL goes back down and I can adjust again to my new data bit. On the other hand, if I want to send a START or a STOP, I set up an initial state during the low part of the cycle as usual, but then instead of just leaving it like that through the next high phase, I transition to the opposite state during the high part of the cycle.
For example, say I am the master and I have finished transmitting my data bytes and I want to send a stop signal to the slave. Then during the next low cycle after I have received the acknowledge for my last data bit I will pull the SDA line low and wait, then during the next high phase of the SCL I will pull the SDA line high. This transition tells everyone on the line that a stop condition has occurred.
You should examine all of the diagrams on pages 208 to 211 and understand the mechanics of the TWI signals. They also discuss "repeated starts", arbitration between multiple masters, and other things which I won't get into since we don't need them yet.
Step 7: Coding the Master
Finally we have enough understanding of what is going on behind the scenes that we can go through my code, line-by-line, for both the Master and the Slave microcontrollers. Please note that if you look at the attached code you will find plentiful comments around everything we discuss here.
Let us begin with some new lines we are adding to our Reset section:
lds temp,PRR<br>andi temp,0b01011111<br>sts PRR,temp<br>ldi temp,255<br>sts TWBR,temp<br>ldi temp,(1--TWPS1)|(1--TWPS0)<br>sts TWSR,temp<br>ldi temp, (1--TWINT)|(1--TWEN)<br>sts TWCR,temp
where you will recall that the "-" signs should be left shift operators.
Okay. We already know what the above code does since we discussed it earlier. The first part simply turns off the power reduction register bits for the timer/counter and the TWI. The second part sets the SCL bit rate and then turns off the TW interrupt and Enables the TWI.
The next thing we do is add a line to our main routine that calls the TWI subroutine. Here 'tis:
main:<br>rcall button_push<br>rcall random<br>rcall dice<br>rcall cycle<br>rcall tw_transmit<br>rcall display<br>rjmp main
Notice that we call the tw_transmit subroutine right after we show the animated dice rolling and just before we display the result of the dice roll on the dice roller LEDs.
Now we get to our Master TWI subroutine:
tw_transmit:<br>ldi temp, (1--TWINT)|(1--TWSTA)|(1--TWEN)<br>sts TWCR,temp
TWEN enables the TWI, TWSTA sends a "start condition" down the wire which is the falling pulse during a high SCL cycle that we discussed earlier, and writing TWINT = 1 to the control register turns off the interrupt flag (i.e. results in TWINT = 0) and starts transmitting a data packet.
This is a subroutine we wrote that just sits around waiting for the TWI interface to complete our command.
lds temp,TWSR<br>andi temp,0b11111000<br>cpi temp,0x08<br>brne ERROR
All this does is check the status register to see what is in there. Whenever the TWI does something it loads the current status of the lines into this register. So you can check and make sure that the command you issued actually made it to the line and was acknowledged by the slave. The second line sets our prescaler bits to zero (remember we have them set to 1 to get our SCL frequency to be 490Hz). It has to set these to zero so that we can compare the status register with the codes given in Table 22-2. Notice that status code 0x08 means the start condition was sent and acknowledged. Well, what is 0x08 in binary? It is 0b00001000. So you see that if we had forgotten to mask the first three bits, our TWSR would contain 0b00001011 = 0x0b and the comparsion would fail. If you didn't set the prescaler bits like we did and just left them as zeros then you wouldn't need to mask these bits. The above ends with a break to an error handler that we have to write which would happen if we didn't get the correct status code. Now let's single out which slave we want to talk to:
ldi temp,0b10100000<br>sts TWDR,temp<br>ldi temp,(1--TWINT)|(1--TWEN)<br>sts TWCR,temp
First we fill the data register, TWDR, with the slave address and the Read/Write bit. The 7-bit slave address that we randomly chose (avoiding the ones the datasheet told us not to use) is 0b1010000 and we tack a 0 on the end to mean we want to "Write" to the slave. If we had tacked a 1 on the end it would mean we want to "Read" from the slave. Then we turn the TWI lines ON by writing TWINT = 1 to the control register to clear the interrupt. The TWEN = 1 is just there so that it stays 1 and the TWI stays "enabled". This will send the 8 bits of data down the SDA line to the slave and the slave will bring the line to ground to acknowledge receipt. We wait for the transmission to complete using our tw_wait routine (discussed below at the end) and check the status register to make sure it all got sent and acknowledged exactly like we did above with the start signal:
rcall tw_wait<br>lds temp,TWSR<br>andi temp,0b11111000<br>cpi temp,0x18<br>brne ERROR
Now we want to actually send our data. Since we sent the address of the slave, and the write bit, it will be sitting there, drooling in anticipation, for 32653 clock cycles ;) ready to read the next 8 bits off the SDA wire and send them directly into it's own TWDR register. We want to send the dice roll. So here is how we do it:
ldi temp,0<br>sts TWDR,temp<br>ldi temp,(1--TWINT)|(1--TWEN)<br>sts TWCR,temp<br>rcall tw_wait<br>lds temp,TWSR<br>andi temp,0b11111000<br>cpi temp,0x28<br>brne ERROR
First we load 0 into the data register, then we turn on the lines and send it. Then we wait for the transmission to complete, and finally we check the status to make sure it was sent and acknowledged. The reason we sent a zero is because we are going to send 2 bytes. The first byte will get loaded into "playercashH" by the slave, and the second byte will get loaded into "playercashL" by the slave. Remember that we eventually want to send two bytes so that we can fill up the 4-digit display. We don't need them this time so we just send a zero for the first one. But I put it in here since it shows how to send more than one byte of data and also we will be using it this way next time.
sts TWDR,dicetotal<br>ldi temp,(1--TWINT)|(1--TWEN)<br>sts TWCR,temp<br>rcall tw_wait<br>lds temp,TWSR<br>andi temp,0b11111000<br>cpi temp,0x28<br>brne ERROR
The above should now be pretty straightforward to understand. We simply load up the data register again, this time with the total roll on the two dice, and send it across, checking the status after the lines go dead. The slave will load this into playercashL. Now we are finished. So we jump to our exit commands:
Here is our ERROR handler. I am assuming this is never going to get executed so all it does is send a Stop signal to the line and then jump back to the very start and try the whole thing again.
ERROR:<br>ldi temp,(1--TWINT)|(1--TWSTO)|(1--TWEN)<br>sts TWCR,temp<br>rjmp tw_transmit
Finally the cleanup and exit:
tw_return:<br>ldi temp,(1--TWINT)|(1--TWSTO)|(1--TWEN)<br>sts TWCR,temp<br>ret
This just sends a stop signal down the line (since we have flipped the TWSTO stop toggle switch), turns global interrupts back on, and returns to "main". To finish up the discussion of the Master code, here is the tw_wait routine:
tw_wait:<br>lds temp,TWCR<br>sbrs temp,TWINT<br>rjmp tw_wait<br>ret
Now let's look at the Slave code.
Step 8: Coding the Slave
Before we get into the TWI subroutine, which in the case of the slave is an interrupt handler, we first set up the rest of the code to use it. First we need to add the interrupt (defined in Table 12-6 on page 65):
This says that if global interrupts are enabled by setting the I bit in SREG to 1, and if, in the TWCR, the TWIE bit is a 1 and the TWI interrupt flag is set this line at location 0x0030 will be executed.
Again we clear the TWI and the Timer/Counter0 bits in the Power Reduction Register PRR as we did with the master. It is not necessary to set the bit rate on the slave since the master controls the SCL frequency and the slave merely responds to it.
sts PRR,temp ldi temp, 0b10100001
Above, we initialize (as per page 223) we load the slave address into the TW Address Register, TWAR. The final bit is a 1 which means that the slave will respond to General Calls. We aren't using general calls here but we may want to use them later when we have more than one slave.
ldi temp, (1--TWEA)|(1--TWEN)|(1--TWIE)
Next we enable the acknowlege bit (so that transmission will be responded to by pulling down the SDA line after receiving the data), enabling the TWI, and the TWI Interrupts (TWIE). Enabling TWI interrupts means that our interrupt handler at .org 0x0030 will be called whenever TWINT is set to 1 by hardware (i.e. the flag is on).
The above lines put our SDA and SCL pins into input mode with internal pullups.
Finally we get to our Slave TWI interrupt handler:
It is always good practice to store your temp registers when entering an interrupt. You don't know when an interrupt will be called and if you are using "temp" in the interrupt it will return to your main code with garbage in it. So push it here and pop it back at the end.
At the bottom you will see what I have for the display_off subroutine. It basically just shuts off the 4-digit displays until the new display numbers are loaded by this handler.
Table 22-4 gives the status codes for a Slave in receiver mode. Here we are just checking to see that it was our address and a write bit that came in from the line and caused this interrupt.
ldi temp, (1--TWINT)|(1--TWEA)|(1--TWEN)
Above we clear the TWI interrupt flag with (1--TWINT), the others are just to keep them on when we load temp into TWCR. If we didn't have (1--TWEA) and (1--TWEN) then they would be set to zero in the TWCR register in the following line. Then we wait until the TWI finishes and becomes available again. Then we check the status register to see that the data byte has been received, stored in TWDR and an acknowledge has been sent back.
This loads the data byte that we got from the line into our playercashH register.
ldi temp, (1--TWINT)|(1--TWEA)|(1--TWEN)
Exactly as above, we receive a data byte, acknowledge it, and verify the status.
This time we store the data byte in playercashL. This will contain our dice roll total.
ldi temp, (1--TWINT)|(1--TWEA)|(1--TWEN)
Turn on the TWI, get the next data byte, check the status to make sure it is the STOP signal and finally jump to our cleanup and exit label.
Our ERROR handler does nothing but load all 1's into the display to show us something went wrong and then return from the interrupt.
Now that we have our display "playercash" registers loaded, we call the "loadcash" routine which will convert them to decimal digits and display them.
ldi temp, (1--TWINT)|(1--TWEA)|(1--TWEN)|(1--TWIE)
The above re-enables the TWIE so that it will once again execute the interrupt handler at 0x0030 when a TWI interrupt occurs. Then we pop our temp back off the stack, re-enable global interrupts, and return to where we were called. The following is our tw_wait subroutine which is identical to the one used in the Master code:
Finally, here is our display_off routine which is self explanatory:
In the next step, I give the code and video of operation.
Step 9: How Does the TWI Interrupt Work?
You should first go to the next step and download the final code and assemble it so that you can use it in this step (I like to keep the final code and video in the last step for those who want to just skip all my babbling and just run the code.)
The TWI is confusing at first because it works differently than other interrupts. Usually when an interrupt occurs and the corresponding interrupt handler is executed, the I flag in SREG is automatically set to 0 so that global interrupts are disabled, then when the the "RETI" is executed to return from the interrupt, the I flag in SREG is automatically set to 1 again to re-enable global interrupts, and also the individual interrupt flag corresponding to the interrupt (i.e. the bit in the control register for that interrupt) is cleared so that the system will again respond to that type of interrupt (see the status register description on page 11). If we allowed interrupts to remain on inside an interrupt we would get "nested interrupts". You could do this by actually re-enabling them while inside your interrupt routine but you wouldn't normally want to do this since another call to the same interrupt would then eject you from the interrupt only to re-enter it at the top.
Exercise 3: Try it! Turn on global interrupts at the start of your TWI routine, then somewhere inside set the TWIE bit in the TWCR register and see what happens the next time the TWINT flag is set.
However, this is not how the TWI interrupt works! TWINT is the interrupt flag. In the TWI it does not remain set during the interrupt handler, it changes all the time. That is because the TWI needs to use the interrupt flag to communicate. So we can't just leave it on. Instead we disable interrupts using TWIE = 0 so that the CPU doesn't eject us back to the interrupt vector when TWINT changes back to a 1. Anyway, this may still seem confusing so we are going to discuss it in the context of our code. Exactly when is TWINT = 1 and when is TWINT = 0 in our TWI routine?
Let's figure it out.
Instead of jumping back and forth in the datasheet to figure out what is going on. Let's use our dice roller and display. Then we will be absolutely clear of what bits are set and when.
Start by adding the following definition to the top of your dice roller code:
.def test = r23
Now go down to the tw_transmit routine and right at the top of it, when the PC first enters the subroutine, put the following line
finally, go down to the two places in the code where we transmit the data to the display. The second one where we transmit the low byte, replace the dicetotal with test. Now when we push the button, the contents of the SREG register, when we first entered the subroutine, will be sent to the display.
Now, when we assemble this and run it, we find that a 0000 shows up on the display. This means that the I flag of SREG is disabled as it should be since we did not enable global interrupts in our dice roller.
Now change the code so that test loads TWCR instead of SREG and view the contents of TWCR.
We find that and that the TWEN flag is the only one on in TWCR. Meaning the TWI is enabled as it also should be since we are using the TWI.
Now lets move our test line to a different location. Move it down to just after we set the TWCR for the first time. This is when we send the start condition. The result is a 36 on our display. This means that TWCR is 0b00100100.
TWCR = (TWINT,TWEA,TWSTA,TWSTO,TWWC,TWEN, - ,TWIE) = (0,0,1,0,0,1,0,0)
So the TWSTA bit is on as it should be since we just loaded the start condition, and the TWEN is on as it also should be. But look! In the previous line we loaded TWINT = 1 into the TWCR register and when we immediately copied the register and viewed it we find that TWINT is 0! Why is that? You see that "setting TWINT = 1" does something different than just setting that bit in TWCR equal to one. If you look on the bottom of page 17 where they describe transmitting a start condition you will see that they say, "TWINT must be written to 1 to clear the TWINT flag"... So what is actually happening here is when you write a 1 to TWINT it sets the value of TWINT equal to zero! That is because TWINT both *controls* the TWINT flag and *is* the TWINT flag. It then says that the TWI will test the 2-wire serial bus and generate a start condition as soon as the bus becomes free. Then, after a start condition has been transmitted, the TWINT flag is set by hardware. This means that TWINT will become 1 when this has finished. We already know that this is the case because our very next line is a tw_wait subroutine that just keeps testing TWINT until it becomes 1 and then reads the status register. The fact that sending a 1 to TWINT only to have it set to 0 is the source of plenty of confusion for people trying to understand how the I2C protocol works. The key is to realize that the TWCR is an active "control" register as well as a bunch of toggle switches. Sending a 1 to TWINT clears the interrupt flag and this flag happens to be the same TWINT bit in the TWCR register so the result is TWINT = 0.
Now lets change our test variable so that it copies the TWSR status register instead. Don't move the location just yet. Just run it as is and find out what is in the status register immediately when we tell the TWCR to transmit a start condition but before that condition has been transmitted.
I get 251. In binary this is 0b11111011. The datasheet says, in section 22.5.5 on page 213, that the TWSR register only contains relevant status information when the TWINT flag is set (remember when we are talking about "flags" then the words "flag is set" means TWINT = 1, when we are talking about "controlling interrupts" then the words "set the flag" would mean writing a 0 to TWINT, resulting in TWINT = 1... bizarre but that is the way its works). Since we are reading TWSR when TWINT = 0, i.e. the interrupt is off and TWI is busy, it means that it should not contain relevant status information, instead, the section goes on to say that it should contain "a special status code indicating that no relevant information is available". Well then. I guess we now know what the "special status code" is. It is 248. In other words all of the bits are set except the 3 that we usually mask. Now a question you might ask is "what about the prescaler bits in TWSR, do they affect this special status code?" remember that we mask them when we want to read the status codes, so if the TWI is just reading the TWSR straight up wouldn't it show those prescaler bits? Well, go up to the top and set the prescaler bits to a different value. You will find that if we set both of the prescaler bits off (so that the prescaler value is now 1 and SCL running at a higher frequency!) and run it we get 248, which is 0b11111000, if we set only TWPS0 to 1 and run it again (this time precaler would be 4) you will find that it now reads 249. If you set only TWPS1 to 1 (so the prescaler is now 16) you will get 250 and with both set as we normally have it we have seen you get 251. So we see that the true "special status code" is 248, meaning all bits of the "status" part of TWSR are set to 1 and the other 3 bits stay as they were.
Now move the "lds test,TWSR" command to after we wait for the interrupt (i.e. after TWINT = 1).
When we push the button we see 0011 show up on the display, i.e. the decimal number 11. What does the status code 11 mean? Well, recall that the last two bits of the TWSR register are our prescaler bits which, in our case, are both 1's. We need to get rid of those (remember how we mask them when we want to check the status?) so lets subtract 3 from our result (3 is what those two bits give when converted to decimal). So the status is actually 8, or 0x08 in hexidecimal. Which is exactly what we expect.
Now move the test line down below the point where we enter the address of the slave into the TWDR register. You will find that it still reads 11. So it remains 11 until we actually do something else with the TWI. Dicking around with the other registers like TWDR doesn't affect the status register. It only changes when the TWI is active.
Now move it below the next time we load the control register and you will find that it becomes the "special status" again.... you get the picture.
As one final test, go to the section where we send the dicetotal to the display and add the following 3 lines to the start of that section:
You see what this does. It keeps the TWEN on so that the TWI remains enabled, but now we write TWINT = 0 in the control register. Then we read out the value of the control register. What do we get? We get 132 showing up on the display! In binary this is 0b10000100. In other words, the TWEN bit is on as we set it, but now the TWINT bit is ON. We sent TWINT = 0 to the control register and the result is TWINT = 1. You now see why right? Sending TWINT = 0 to the control register actually SETS the interrupt flag. So the result is the interrupt flag is ON, it is a 1.
Okay. Now we know what is happening in the dice roller with the TWINT bit of the TWCR register. The only potentially confusing part was when we set TWINT = 1 and then read it only to find its value was 0 and when we set TWINT = 0 we found its value was 1. I think you now see why this is and how it works. Setting values to the TWCR is the same as sending commands to the TWI. The TWINT = 1 command means "execute the command in the TWCR", but the value that get set in the TWINT bit when that command is executed is the "clear the interrupt flag" which means "start up the TWI and send some data". As far as the interrupt flag (the actual value in the bit) is concerned, TWINT = 0 means interrupt flag is off, the TWI is busy doing something. When TWINT goes back to 1 it means the interrupt flag is set and you can now modify things again. On the other hand, as far as the *commands* you send to the TWCR, writing TWINT = 0 *sets* the interrupt flag and results in TWINT = 1, and the TWI turns off allowing you to modify the data register. Have I beat the dead horse long enough? I only do this because it is as confusing as f... well. you get the idea. If you are still confused just play with it for awhile.
Now examine Figure 22-10 which shows the things happening during a typical transmission. Now that we understand how TWINT works it is clear what the actually value in that bit is at any given moment during the transmission. In the white sections TWINT = 0, and in the black sections TWINT = 1. Whenever the description says to "make sure that TWINT is written to one" you now know that this results in TWINT = 0, i.e. the interrupt flag is off, or "unset". The above quoted statement about making sure that TWINT is written to one must be the source of nothing but confusion to people when they are trying to figure out how the TWI works. It is annoying that the writers of the datasheet didn't take the time to really explain what is happening. Other interrupt flags in AVR work the same way, for example the Timer/Counter overflow flag that we dealt with in previous tutorials. Writing a 1 to the TOV0 bit of the TIFR0 Timer/Counter0 Interrupt Flag Register also *clears* the flag and so results in a logic 0 in that bit. However in the case of other interrupt flags we don't usually need to set and clear them with software, the hardware takes care of that. It is only because in the case of the TWI we need to set and clear the flag while inside the actual interrupt routine that we need to worry about it and there exists the potential for confusion.
Let's now restore our dice roller code to the way it was and move over to our 4-digit display.
In the 4-digit-display code add a new general purpose register called "test" as we did with the dice roller. Then in our tw_int interrupt handler, add the following two lines at the beginning of our tw_return label, i.e at the end just before you go back to main:
so that the display will show whatever is in the test register. Now lets test some things.
First let's test SREG in various places in the interrupt handler. Put "lds test,SREG" in right at the top when the interrupt is first called. You will find that it reads 0. This means that the "I" flag is automatically cleared when we enter the interrupt handler. You will also find that it remains off during the entire interrupt. This is the normal way that SREG behaves.
Now let's test TWSR. Here is where we find something that may seem strange. If we test it just after the interrupt is called we get 96 showing up. Why is this? Well, you will recall that TWSR status codes are in hexidecimal. If you convert 96 to hexidecimal you will find that it is 60. Which is the value that we should get when the interrupt is first called.
Exercise 4: What is the "special status code" used in the slave receiver?
Now look at the TWDR register when you first enter the interrupt. You will find that it is equal to the address that we have set up for the slave, as it should be since reading the address from the TWI into the data register is how we knew we were called.
Finally, let's look at TWCR and the TWINT bit. If you put "lds test,TWCR" right at the beginning when the handler is first entered you get 197 which is 0b11000101. This means that TWINT = 1, TWEA = 1, TWEN = 1, and TWIE = 1. This is exactly how we set things up in our Reset section of the program (go look, see?) except that now the TWINT interrupt flag is ON. Which is also normal since we have just entered the interrupt, we wouldn't be here if something didn't set that flag.
Now go to the top of your program to the Reset section and add the following two lines:
as if we were going to initialize our data register TWDR to zero. What happens now when you check the TWCR register at the beginning of the interrupt? You get 205.
Exercise 5: Why do you get 205?
Now you might like to test the values of registers elsewhere in the code. I think you will find that they are as you expect.
Step 10: Final Code and Video
I have attached a video showing the operation of the dice roller as Master controller hooked up to the 4-digit display as the Slave controller. It simply shows how the numerical result of a roll of the dice shows up on the display.
I have also attached the (commented) code for both controllers.
That is it for this time! I hope you enjoyed it. Next time we will finally be making the first working part of our final project. We will be adding another controller to the mix and using the TWI to set up a Master/Slave 3-way between them all.
See you next time! ... and Merry Christmas!