Introduction: AVR Assembler Tutorial 9

About: I am interested in a wide range of things as shown in my list of interests. Almost anything creative is fun and worth trying.

Welcome to Tutorial 9.

Today we will be showing how to control both a 7-segment display and a 4-digit display using our ATmega328P and AVR assembly language code. In the course of doing this we will have to take diversions in to how to use the stack to reduce the number of registers that we need to tie up. We will add a couple of capacitors (low-pass filters) to try to reduce the noise on our keypad. We will create a voltage amplifier out of a couple of transistors so that our INT0 interrupt switch works better for the lower voltage buttons on the bottom row of the keypad. And we will bang our heads against the wall a bit trying to get the correct resistors so that the thing works properly.

We will be using our keypad from Tutorial 7

To do this tutorial, in addition to the standard stuff, you will need:

  1. A 7-segment display

    https://www.sparkfun.com/products/8546

  2. A 4-digit display

    https://www.sparkfun.com/products/11407

  3. A pushbutton

    https://www.sparkfun.com/products/97

  4. The datasheets for the display which can be downloaded from their respective pages linked to above.
  5. A 68 pf ceramic capacitor, a couple of 104 capacitors, a bunch of resistors, two 2N3904 NPN transistors.

Here is a link to the complete collection of my AVR assembler tutorials: https://www.instructables.com/id/Command-Line-AVR-T...

Step 1: Wiring the 7-seg Display

We are going to use the same code that we used in Tutorial 7 for the keypad to control the 7-segment display. So you will need to make a copy of that and we will modify it.

We will map the segments to the pins of our microcontroller as follows:

(dp,g,f,e,d,c,b,a) = (PD7,PD6,PB5,PB4,PB3,PB2,PB1,PB0)

where the letters of the segments are shown in the picture along with the pinout corresponding to common 5V and each of the LED segments including the decimal point (dp) at the lower right of the display. The reason for this is so that we can input the entire number into a single register and output that register to ports B and D to light up the segments. As you can see the bits are numbered sequentially from 0 to 7 and so they will map to the correct pins without having to set and clear individual bits.

As you can see by the code we have attached in the next step, we have moved our display routine to a macro and we have freed up the SDA and SCL pins for future use in the next Tutorial.

I should add that you need to put a resistor between the common anode of the display and the 5V rail. I chose a 330 ohm resistor as usual but if you like you could calculate the minimum resistance needed to get the maximum brightness out of the display without frying it. Here is how to do that:

First look at the data sheet and notice that on the first page it gives various properties of the display. The important quantities are the "Forward Current" (I_f = 20mA) and the "Forward Voltage" (V_f = 2.2V). These tell you want the voltage drop across the display will be if the current is equal to the forward current. This is the maximum current that the display will take without frying. It is consequentially also the maximum brightness you can get out of the segments.

So let's use Ohm's law and Kirchoff's loop rule to figure out what minimum resistance we would need to put in series with the display to get the max brightness. Kirchoff's rule says that the sum of the voltage changes around a closed loop in a circuit equals zero and Ohm's law says that the voltage drop across a resistor of resistance R is: V = I R where I is the current flowing through the resistor.

So given a source voltage of V and going around our circuit we have:

V - V_f - I R = 0

which means (V - V_f)/I = R. So the resistance needed to get the maximum brightness (and probably frying the segments) would be:

R = (V - V_f)/I_f = (5.0V - 2.2V)/0.02A = 140 ohms

So if you wanted to you could happily use 150 ohms without worries. However, I think 140 ohms makes it too bright for my liking and so I use 330 ohms (which is sort of my personal Goldilocks resistance for LEDs)

Step 2: Assembly Code and Video

I have attached the assembly code and a video showing the operation of the keypad with the display. As you can see we have simply mapped the Redial key to "r", the flash key to "F", the asterisk to "A" and the hash sign to "H". These could be mapped to various operations like backspace, enter, and what-not if you wanted to continue to use the keypad for typing numbers on LCD displays or 4 digit displays. I won't go through the code line-by-line this time since it is very similar to what we have already done in previous tutorials. The differences are mainly just more of the same things we already know how to do like interrupts and look-up tables. You should just go through the code and look at the new things we have added and the things we have changed and figure it out from there. We will go back to line-by-line analysis in the next tutorial when we introduce new aspects of assembly language coding on AVR microcontrollers.

Let's now look at a 4-digit display.

Step 3: Wiring the 4-digit Display

According to the datasheet, the 4-digit display has a Forward Current of 60 mA and a forward voltage of 2.2 volts. So, by the same calculation as before, I could use a 47 ohm resistor if I wanted to. Instead I am going to use a... hrm.. let me see... how about 330 ohms.

The way that the 4-digit display is wired is that there are 4 anodes, one for each of the digits, and the other pins control which segment comes on in each. You can display 4 digits simultaneously because they are multiplexed. In other words, just like we did for the pair of dice, we simply cycle the power through each of the anodes in turn and it will blink them on one after the other. It will do this so fast that our eyes won't see the blinking and it will look like all four digits are on. However, just to be sure, the way we will code it is to set all four digits, then cycle the anodes, rather than set, move, set, move, etc. That way we can get a precise timing between lighting up each digit.

For now, let's test that the segments all work.

Place your 330 ohm resistor between the positive rail of your breadboard and the first anode on the display. The datasheet tells us that the pins are numbered from 1 to 16 counter-clockwise starting at the bottom left (when you are looking at the display normally.. with the decimal points along the bottom) and it states that the anodes are pin numbers 6, 8, 9, and 12.

So we connect pin 6 to 5V and then take a negative lead from your GND rail and poke it in to all of the other pins and see that all of the segments light up on the digit it corresponds to (which is actually the second digit from the right). Make sure you get all 7 segments and the decimal point to light up.

Now stick your GND wire into one of the pins to light up one of the segments and this time move the resistor around to the other 3 anodes and see that the same segment lights up in each of the other digits.

Anything unusual?

It turns out that the pinout on the datasheet is wrong. This is because it is the datasheet and pinout for a 12-pin, 4-digit display. I.e. one with no colon or upper decimal point. The display that I got when I ordered it is a 16 pin, 4-digit display. In fact, on mine, the segment anodes are at pins 1, 2, 6, and 8. The colon anode is pin 4 (cathode pin 12) and the upper dp anode is pin 10 (cathode is pin 9)

Exercise 1: Use your resistor and ground wire to map out which pin corresponds to which segment and decimal point on the display so we get the correct segments lighting up when we code it.

The way that we want to code the segment map is exactly like we did with the single digit 7-segment display above -- we don't have to change a thing in the code, the only thing we change is how the wires are connected on the board. Simply plug the correct port pin on the microcontroller to the corresponding pin on the 4-digit display so that, for example, PB0 still goes to the pin corresponding to segment a, PB1 goes to segment B, etc.

The only difference is that now we need 4 extra pins for the anodes since we can't simply go to the 5V rail any more. We need the microcontroller to decide which digit gets the juice.

So we will use PC1, PC2, PC3, and PD4 to control the anodes of the 4 digits.

You might as well go ahead and plug in the wires. (don't forget the 330 ohm resistors on the anode wires!)

Step 4: Coding the 4-digit Display

Let's think about how we want to code this display.

We would like the user to push keypad buttons and have the numbers appear sequentially on the display as they push each button. So If I push a 1 followed by a 2 it will show up on the display as 12. I would also like to store that value, 12, for internal use but we will get to that a bit later. For now I just want to write a new macro which takes your keypresses and displays them. However, since we only have 4 digits I want to make sure it only allows you to type four numbers.

Another issue is that the way the multiplexed 4-digit display works is by cycling the anodes so that each digit is only on for a split second before it displays the next and then the next and finally back to the first again, etc. So we need a way to code this.

We also want it to move the "cursor" over to the right a space when we type the next digit. So that if I want to type 1234 for example, after I type the 1, the cursor will move over so that the next digit I type will appear on the next 7-segment display and so on. All while this is happening I still want to be able to see what I have typed so it still has to be cycling through the digits and displaying them.

Sound like a tall order?

Things are actually even worse. We need 4 more general purpose registers that we can use to store the current values of the 4 digits we want to display (if we are going to cycle through them we have to keep them stored somewhere) and the problem with this is that we have been using up general purpose registers like crazy and if we don't watch out we won't have any left. So it is probably a good idea to tackle that issue sooner rather than later and showing you how to free up registers by using the stack.

So let's start by simplifying things a bit, use the stack, and free up some registers and then we will try to accomplish the task of reading and displaying our numbers on the 4-digit display.

Step 5: Push 'n Pop

There are only a few "General Purpose Registers" that we have at our disposal and once they are used there are no more. So it is good programming practice to only use them for a couple variables that are used as temporary storage that you need for reading from, and writing to, ports and SRAM with, or else ones that you will be needing in subroutines everywhere and so you name them. So what I have done, now that we have initialized and are learning to use the Stack, is to go through the code and find the named general purpose registers that are used only inside a single subroutine or interrupt and nowhere else in the code and replace them with one of our temp registers and a push and pop to the stack. In fact, if you look at code written for smaller microcontrollers, or if you go back in time to when all chips were smaller, you will see only a couple of general purpose registers that had to be used for everything, so you couldn't just store a value in there and leave it alone since you were sure to need that register for other things. So you will see pushin' and a poppin' all over the place in the code. Maybe I should have named our temp general purpose registers AX and BX as a respectful kudos to those bygone days.

An example will help make this more clear.

Notice that in our Analog to Digital conversion complete interrupt ADC_int we use a general purpose register that we have named buttonH which we used to load the value of ADCH and compare it with our lookup table of analog to button press conversions. We only use this buttonH register within the ADC_int subroutine and nowhere else. So instead we will use our variable temp2 which we use as a temporary variable that we can use within any given subroutine and its value won't affect anything outside of that subroutine (i.e. the value we give it in ADC_int won't be used anywhere else).

Another example is in our delay macro. We have a register we have named "milliseconds" which contains our delay time in milliseconds. In this case it is in a macro and we recall that the way macro's work is that the assembler places the entire macro code into the spot of the program where it is called. In this case we would like to get rid of the "milliseconds" variable and replace it with one of our temporary variables. In this case I will do it a bit differently to show you how even if the value of the variable is going to be needed elsewhere we can still use it by using the stack. So instead of milliseconds we use "temp" and in order that we don't screw up other things that also use the value of temp we simply begin the "delay" macro by "pushing" temp on to the stack, then we use it instead of milliseconds, and then at the end of the macro we "pop" its previous value back from the stack.

The net result is that we have "borrowed" temp and temp2 for temporary use and then restored them to their previous values when we are finished.

Here is the ADC_int interrupt routine after making this change:

ADC_int:
  push temp            ; save temp since we modify it here
  push temp2           ; save temp2
  lds temp2,ADCH       ; load keypress
  ldi ZH,high(2*numbers)
  ldi ZL,low(2*numbers)
  cpi temp2,0
  breq return          ; if noise triggers don't change 7segnumber
 setkey:
  lpm temp,Z+          ; load from table and post increment
  clc
  cp temp2,temp        ; compare keypress with the table
  brlo PC+4            ; if ADCH is lower, try again 
  lpm 7segnumber,Z     ; otherwise load keyvalue table
  inc digit           ; increment the digit number
  rjmp return          ; and return
  adiw ZH:ZL,1         ; increment Z
 rjmp setkey           ; and go back to top
 return:
  pop temp2            ; restore temp2
  pop temp             ; restore temp
reti

Notice that the way the stack works is that the first on is the last off. Just like a stack of papers. You see that in our first two lines we push the value of temp on to the stack, then we push temp2 on to the stack, then we use them in the subroutine for other things, and finally we restore them to their previous values again by first popping temp2 off (since it was the last one pushed on it is at the top of the stack and will be the first one we pop back off) and then popping temp.

So from now on we will always use this method. The only time we will actually designate a register for something other than a temp variable is when we are going to need it everywhere. For example, the register called "overflows" is one that we use in several different places in the program and so we would like to give it a name. Of course we could still use it the way we have done with temp and temp2 since we would restore it's value after we are done. But that would spaghettify things too much. They are named for a reason and we have temp and temp2 already designated for that job.

Step 6: Low-pass Filters and Voltage Amplifier

In order to clean up the noise a bit and make our keypad work better we want to add a couple of low-pass filters. These filter out the high frequency noise and allow the low frequency signal to pass through. Essentially the way to do this is simply to add a 68 pf capacitor between our analog input and ground and also a 0.1 microfarad (i.e. 104) capacitor between our PD4 (INT0) interrupt and ground. If you play around with these while pushing buttons on the keypad you will be able to see what they do.

Next we want to make a voltage amplifier. It turns out that the bottom row of keys on the keypad (as well as the redial key) are putting out too low of a voltage to trip the INT0 interrupt. The analog port is sensitive enough to read the low voltages from these keys but our interrupt pin is not getting a good enough rising edge to interrupt when we push those keys. Hence we would like some way of making sure that a nice voltage rising edge hits PD4 but the same low voltage hits ADC0. This is a pretty tall order since both signals are coming from the same output wire of our keypad. There are a number of sophisticated ways to do this, but we are not going to be using our keypad anymore after this tutorial so let's just kluge together a method that works (barely).

You should first hook up an external button to replace the INT0 interrupt and control the display by holding a key on the keypad and clicking the button. This has fewer keypad problems and will allow you to be confident that your voltages are set correctly on the keypad look-up table. Once you know the keypad is wired correctly then get rid of the button and put the INT0 interrupt back. There are some serious noise and voltage issues controlling the keypad this way so it is good to know that everything works so that future problems can be isolated to the INT0 key.

When you wire your keypad and your voltage amplifier, it is very likely that the same resistor values that I have used are not going to work. So you will have to do some experimentation to get values that work for you.

If you look at the diagram I have attached to this step you will see how the voltage amplifier is going to work. We use some resistors and two transistors. The way transistors work (see the data sheets!) is there is a minimum voltage that you need to input to the base pin on the transister (the middle pin) which will saturate it and allow current to flow between the collector pin and the emitter pin. In the case of the 2N3904 transistor that we are using here the voltage is 0.65V. Now we are taking that voltage from our output from the keypad and we don't want to change that output so we will put a big resistor between the output from the keypad and the base of the first transistor (I used 1Mohm). I have labeled this as R_1 in the diagram. Then we want to set up a voltage divider so that the base of the transistor is "almost" at 0.65 volts already and only a teeny weeny bit more will push it over the top and saturate it. That teeny weeny bit will come from the output of the keypad when we push a button. Since the lower keys on the keypad are only putting out a tiny voltage we need to be very close to saturation already in order for them to be enough. The voltage divider resisters are labeled R_a and R_b on the diagram. I used R_a = 1Mohm and R_b = 560Kohm but it is almost certain that you will have to play around with these numbers to get it right for your setup. You may want to have a wall nearby to bang your head against and two or three glasses of scotch on hand (I would recommend Laphroaig -- expensive, but worth it if you like smoke. If things get really insane, then just get a jug of BV and settle in for the night)

Now lets look at how the transistors are going to get us a nice rising edge going in to the INT0 key and generate our keypress interrupt. First lets look at what happens when I am not pressing a key. In that case the first transistor (labeled T1 in the diagram) is off. So no current is flowing between the collector and emitter pins. Thus the base of the other transistor (labeled T2) will be pulled high and thus it will saturate allowing current to flow between its pins. This means that the emitter of T2 will be pulled low since it is connected to the collector which itself is connected to ground. Thus, the output that goes to our INT0 keypress interrupt pin (PD4) will be low and there will be no interrupt.

Now what happens when I push a key? Well then the base of T1 goes above 0.65V (in the case of the lower keys it only barely goes above!) and then current will be allowed to flow which will pull the base of T2 to low voltage and this will shut off T2. But we see that when T2 is off, then the output is pulled high and hence we will get a 5V signal going to our INT0 pin and it will cause an interrupt.

Notice what the net result is here. If we push the 1 key, we get 5V going to PD4 without significantly changing the output going to ADC0, and more importantly, even if we push Asterisk, 0, Hash, or Redial, we also get a 5V signal going to INT0 and also causing an interrupt! This is important since if we just went directly from the keypad output to the INT0 pin, those keys are generating almost no voltage and they won't be enough to trigger that interrupt pin. Our voltage amplifier has solved this problem.

Step 7: 4-digit Display Code and Video

That is all for tutorial 9! I have attached the code and a video showing the operation.

This will be the last time we will use the analog keypad (thank god). It was tough to use, but it was also very useful to help us learn about analog-to-digital conversion, analog ports, interrupts, multiplexing, noise filters, voltage amplifiers, and many aspects of assembly coding from lookup tables to timer/counters, etc. That is why we decided to use it. (plus it is fun to scavenge stuff).

Now we are going to look at communication again and get our 7-segment and our 4-digit displays to read out our dice rolls from our dice roller the same way that we did with our register analyzer. This time we will use the two-wire interface rather than our hacked together morse code method.

Once we have the communications working and the rolls showing up on the displays we can finally make the first piece of our final product. You will notice that without all of the analog port stuff our code is going to be significantly shorter and probably easier to read.

For those of you who are ambitious. Here is a "project" that you could try that you certainly have the knowledge to do at this point if you have gone through all of these tutorial to this point:

Project: Make a calculator! Use our 4-digit display and our keypad and add an external button push that will act like an "enter" key. Map the asterisk to "times", the hash to "divide" the redial to "plus" and the flash to "minus" and write a calculator routine that acts like one of those old HP "reverse polish" calculators that all the engineers had back in the day. I.e. the way they work is that you enter a number and press "enter". This pushes that number on to the stack, then you enter a second number and push "enter", which pushes the second number on to the stack. Finally you press one of the operations like X, /, + or - and it will apply that operation to the top two numbers on the stack, display the result, and push the result onto the stack so that you can use it again if you like. For example to add 2+3 you would do: 2, "enter", 3, "enter", "+" and the display would then read 5.
You know how to use the stack, the display, the keypad, and you have most of the background code already written. Just add the enter key and the subroutines needed for the calculator. It is a bit more complicated than you might at first think, but it is fun and do-able.

See you next time!

Formlabs Contest

Participated in the
Formlabs Contest