Implementation of avr software timers. AVR. Training course. Timers. Generating time intervals using a timer

Let's look at how to make a timer with your own hands on the ATmega8 microcontroller, although the code is quite easy to adapt for AVR MKs of other series. An electronic timer is a necessary device in all areas where it is necessary to perform certain actions after a specific period of time.

The timer control consists of only four buttons:

— increasing the value of the number;

— decreasing the value of the number;

— start timer;

— timer reset.

An audio frequency generator with a speaker is used as an indicator of the timer operation. The generator will be started using transistor switch Q5, which in turn is opened by a positive potential coming from the PC2 port of the microcontroller.

Simplified, the timer works as follows. Use the “+” and “-” buttons to set the required number of seconds; The “start” button starts the timer. When the timer counts down to zero, a high potential will appear on the PC2 pin of the ATmega8 microcontroller, which will open Q5. Next, the transistor switch will start the generator and a sound will be heard in the speaker. The timer is reset by pressing the “reset” button. The audio frequency generator is assembled on two transistors Q6 and Q7 of different semiconductor structures. The operating principle and description of the circuit of such generators can be found by clicking on.

Timer operation algorithm on a microcontroller

Our timer will count down exactly one second at a time, although you can set any other time, for example minutes, hours, hundredths of seconds, etc.

To form a time interval of one second, we will use the first timer-counter of the ATmega8 microcontroller. We will define all its settings in the function start. First, we divide the operating frequency of the microcontroller 1000000 Hz by 64 and get a new frequency of 15625 Hz. Bits CS10, CS11 and CS12 of the TCCR1B register are responsible for this. Next, we enable the coincidence interrupt and write a binary number equal to the decimal 15625 into the comparison register (high and low) comparison registers.

void start (void)

TCCR1B &= ~(1<

TCCR1B |= (1<

TIMSK |= (1<

OCR1AH ​​= 0b00111101;

OCR1AL = 0b000001001; // comparison register 15625

TCNT1 = 0;

TCCR1B |= (1<

When the timer counts down exactly one second, an interrupt will be called. In the body of the interrupt function, we will decrease the value of the variable by one. When zero is reached, a high potential will appear on the second output of port C of the microcontroller, which will open the transistor switch and start the generator, as a result of which we will hear sound in the speaker.

ISR (TIMER1_COMPA_vect)

Z—;

We figured out the iteration counter of the main loop and found out that it is completely unsuitable for accurate time readings - the shutter speed floats, and it’s difficult to count it. What to do?

Obviously, we need some kind of external counter that would tick regardless of the processor’s operation, and the processor could at any time see what was ticking in it. Or for the counter to generate overflow or underflow events - raise the flag or generate an interrupt. And the percent will smell it and process it.

And there is such a counter, not even one - these are peripheral timers. There may be several of them in an AVR, and even with different bit depths. ATmega16 has three, ATmega128 has four. And in the new MKs of the AVR series there may be even more, I didn’t recognize it.

Moreover, a timer can be more than just a stupid counter; a timer is one of the most sophisticated (in terms of alternative functions) peripheral devices.

What can timers do?

  • Tick ​​at different speeds, counting the time
  • Count incoming pulses from outside (counter mode)
  • Tick ​​from external quartz at 32768Hz
  • Generate several types of PWM signal
  • Issue interrupts (half a dozen different events) and set flags

Different timers have different functionality and different bit depths. See the datasheet for more details.

Timer tick source
The Timer/Counter (hereinafter I will call it T/C) counts either clock pulses from the built-in clock generator or from the counting input.

Look carefully at the pinout of the ATmega16 legs, do you see legs T1 and T0 there?

So these are the counting inputs Timer 0 and Timer 1. With appropriate settings, the T/S will count either the leading edge (edge ​​from 0-1) or the trailing edge (edge ​​of 1-0) of the pulses arriving at these inputs.

The main thing is that the frequency of incoming pulses does not exceed the clock frequency of the processor, otherwise it will not have time to process the pulses.

In addition, T/C2 is capable of operating in asynchronous mode. That is, the T/S does not count the clock pulses of the processor, not the incoming pulses to the legs, but the pulses of its own oscillator, powered by a separate quartz. To do this, T/C2 has inputs TOSC1 and TOSC2, on which you can hang a quartz resonator.

Why is this even necessary? Yes, at least organize a real time clock. I hung a clock quartz on them at 32768 Hz and count the time - 128 overflows will occur per second (since T/C2 is eight-bit). So one overflow is 1/128 of a second. Moreover, the timer does not stop while the overflow interrupt is being processed; it also continues to count. So the clock is a breeze!

Predivider
If the timer counts pulses from a clock generator, or from its internal one, then they can still be passed through a prescaler.

That is, even before entering the counting register, the pulse frequency will be divided. You can divide by 8, 32, 64, 128, 256, 1024. So if you put a clock quartz on T/C2 and pass it through a prescaler at 128, then your timer will tick at a speed of one tick per second.

Comfortable! It’s also convenient to use a prescaler when you just need to get a large interval, and the only source of ticks is the processor clock generator at 8 MHz, you’ll get tired of counting these megahertz, but if you pass it through the prescaler, at 1024, then everything is much happier.

But there is one peculiarity here, the fact is that if we launch the T/S with some kind of brutal prescaler, for example at 1024, then the first tick to the counting register will not necessarily come after 1024 pulses.

It depends on what state the prescaler was in, what if by the time we turned it on it had already counted to almost 1024? This means there will be a tick right away. The prescaler works all the time, regardless of whether the timer is turned on or not.

Therefore, the prescalers can and should be reset. You also need to take into account the fact that the prescaler is the same for all counters, so when resetting it, you need to take into account the fact that another timer will lose time until the next tick, and it can go wrong in exactly this way.

For example, the first timer operates on pin 1:64, and the second on pin 1:1024 of the prescaler. The second one almost reached 1024 in the prescaler and now there should be a timer tick, but then you went and reset the prescaler to start the first timer exactly from scratch. What will happen? That's right, the second divider will immediately reset to 0 (the prescaler is the same, it has one register) and the second timer will have to wait another 1024 clock cycles to get the desired impulse!

And if you reset the prescaler in the loop, for the benefit of the first timer, more often than once every 1024 clock cycles, then the second timer will never tick, and you will be hitting your head on the table, trying to understand why your second timer is not working, although must.

To reset the prescalers, simply write the PSR10 bit in the SFIOR register. The PSR10 bit will be reset automatically on the next clock cycle.

Account register
The entire result of the torment described above is accumulated in the TCNTx counting register, where x is the timer number. it can be either eight-bit or sixteen-bit, in which case it consists of two registers TCNTxH and TCNTxL - the high and low bytes, respectively.

And there is a catch here, if you need to put a number in an eight-bit register, then there are no problems OUT TCNT0, Rx and no nails, then with two-byte registers you will have to play around.

And the whole point is that the timer counts independently of the processor, so we can put one byte first, it will start counting, then the second, and the recalculation will begin taking into account the second byte.

Feeling bad? Here! The timer is a precise device, so its counting registers must be loaded at the same time! But how? And engineers from Atmel solved the problem simply:
The high register (TCNTxH) is written to first in the TEMP register. This register is purely official and is in no way accessible to us.

What we end up with: We write the high byte to the TEMP register (for us this is one hell of a TCNTxH), and then write the low byte. At this moment, the value we previously recorded is entered into the real TCNTxH. That is, two bytes, high and low, are written simultaneously! You cannot change the order! The only way

It looks like this:

CLI ; We prohibit interruptions, without fail! OUT TCNT1H,R16 ; The most significant byte was written first to TEMP OUT TCNT1L,R17 ; And now I’ve signed up for both senior and junior classes! SEI ; Enable interrupts

Why disable interrupts? Yes, so that after writing the first byte, the program does not accidentally rush off without interruption, and then someone rapes our timer. Then in its registers it will not be what we sent here (or in the interrupt), but what the hell. So try to catch such a bug later! But it can come out at the most inopportune moment, but you won’t catch it, because an interruption is almost a random variable. So such moments need to be addressed immediately.

Everything is read the same way, only in reverse order. First, the low byte (while the high one is shoved into TEMP), then the high one. This guarantees that we are counting exactly the byte that was currently in the counting register, and not the one that was running while we were picking it out byte byte from the counting register.

Control registers
I won’t describe all the functions of timers, otherwise it will turn out to be an overwhelming treatise. It’s better to talk about the main one - the counting one, and all sorts of PWM and other generators will be in another article. So be patient, or chew on the datasheet, it’s also useful.

So the main register is TCCRx
For T/C0 and T/C2 these are TCCR0 and TCCR2, respectively, and for T/C1 this is TCCR1B

For now, we are only interested in the first three bits of this register:
CSx2.. CSx0, replace x with the timer number.
They are responsible for setting the prescaler and the clock source.

Different timers are slightly different, so I will describe the CS02..CS00 bits only for timer 0

  • 000 - timer stopped
  • 001 — the prescaler is equal to 1, that is, off. timer counts clock pulses
  • 010 - prescaler is 8, clock frequency is divided by 8
  • 011 - prescaler is 64, clock frequency is divided by 64
  • 100 - prescaler is 256, clock frequency is divided by 256
  • 101 - prescaler is 1024, clock frequency is divided by 1024
  • 110 - clock pulses come from the T0 pin at the transition from 1 to 0
  • 111 - clock pulses come from the T0 pin at the transition from 0 to 1

Interrupts
Every hardware event has an interrupt, and the timer is no exception. As soon as an overflow or some other curious event occurs, an interrupt immediately appears.

The TIMSK and TIFR registers are responsible for interrupts from timers. And cooler AVRs, such as ATMega128, also have ETIFR and ETIMSK - a kind of continuation, since there will be more timers there.

TIMSK is a mask register. That is, the bits contained in it locally enable interrupts. If the bit is set, the specific interrupt is enabled. If the bit is zero, then this interrupt is covered with a basin. By default, all bits are zero.

At the moment, we are only interested in overflow interrupts. The bits are responsible for them

  • TOIE0 - permission to interrupt on timer 0 overflow
  • TOIE1 - permission to interrupt on timer 1 overflow
  • TOIE2 - permission to interrupt on timer 2 overflow

We'll talk about other features and timer interrupts later, when we look at PWM.

The TIFR register is directly a flag register. When some kind of interrupt is triggered, a flag pops up indicating that we have an interrupt. This flag is reset by hardware when the program leaves the vector. If interrupts are disabled, then the flag will remain there until interrupts are enabled and the program goes to the interrupt.

To prevent this from happening, the flag can be reset manually. To do this, you need to write 1 in the TIFR register!

Now let's fuck
Well, let's redesign the program to work with a timer. Let's introduce a program timer. The barrel organ will remain so, let it tick. And we will add a second variable, also four bytes:

ORG $010 RETI ; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RJMP Timer0_OV ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete

Let's add an interrupt handler for timer 0 overflow to the Interrupt section. Since our ticking macro actively works with registers and corrupts flags, we need to save this whole thing on the stack first:

By the way, let's create another macro that pushes the SREG flag register onto the stack and a second one that retrieves it from there.

1 2 3 4 5 6 7 8 9 10 11 12 .MACRO PUSHF PUSH R16 IN R16,SREG PUSH R16 .ENDM .MACRO POPF POP R16 OUT SREG,R16 POP R16 .ENDM

MACRO PUSHF PUSH R16 IN R16,SREG PUSH R16 .ENDM .MACRO POPF POP R16 OUT SREG,R16 POP R16 .ENDM

As a side effect, it also retains R16, remember that :)

1 2 3 4 5 6 7 8 9 10 11 12 13 Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI

Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI

Now initialize the timer. Add it to the Internal Hardware Init section.

; Internal Hardware Init ====================================== SETB DDRD,4,R16 ; DDRD.4 = 1 SETB DDRD,5,R16 ; DDRD.5 = 1 SETB DDRD,7,R16 ; DDRD.7 = 1 SETB PORTD,6,R16 ; PD6 output to pull-up input CLRB DDRD,6,R16 ; To read the button SETB TIMSK,TOIE0,R16 ; Enable timer interrupt OUTI TCCR0,1<

All that remains is to rewrite our comparison block and recalculate the number. Now everything is simple, one tick one bar. Without any problems with different code lengths. For one second at 8 MHz, 8 million ticks must be made. In hexes this is 7A 12 00, taking into account that the low byte is TCNT0, then 7A 12 is left for our counter, and also the highest two bytes 00 00, they don’t need to be checked. There is no need to mask; we will reset the timer later anyway.

There is only one problem - the low byte, the one in the timer. It ticks every tick and it will be almost impossible to check for compliance. Because the slightest discrepancy and the comparison condition will appear in NoMatch, but to guess it so that the check of its value coincides with this particular step... It’s easier to pull a needle out of a haystack on the first try at random.

So the accuracy in this case is limited - you need to have time to check the value before it leaves the range. In this case, the range will be, for simplicity, 255 - the value of the low byte, the one in the timer.

Then our second is provided with an accuracy of 8,000,000 plus or minus 256 cycles. The error is not large, only 0.003%.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 ; Main ================================================================= ======== Main: SBIS PIND,6 ; If the button is pressed - transition RJMP BT_Push SETB PORTD,5 ; Let's light LED2 CLRB PORTD,4 ; Turn off LED1 Next: LDS R16,TCNT ; Load numbers into registers LDS R17,TCNT+1 CPI R16.0x12 ; Let's compare byte by byte. First byte BRCS NoMatch ; If it’s less, it means it didn’t hit. CPI R17.0x7A ; Second byte BRCS NoMatch ; If it’s less, it means it didn’t hit. ; If it matches, then we do the Match action: INVB PORTD,7,R16,R17 ; Inverted LED3; Now we need to reset the counter, otherwise during the same iteration of the main loop; we will get here more than once - the timer will not have time to reach 255 values; so that the number in the first two bytes of the counter changes and the condition is triggered. ; Of course, you can bypass this with an additional flag, but it’s easier to reset the counter :) CLR R16 ; We need zero CLI; Access to a multibyte variable; simultaneously from interruption and background; Atomic access is needed. Disable interrupts OUTU TCNT0,R16 ; Zero to the timer counter register STS TCNT, R16 ; Zero in the first byte of the counter in RAM STS TCNT+1,R16 ; Zero in the second byte of the counter in RAM STS TCNT+2,R16 ; Zero in the third byte of the counter in RAM STS TCNT+3,R16 ; Zero in the first byte of the counter in RAM SEI; Let's enable interrupts again. ; If it doesn't match, we don't do it :) NoMatch: NOP INCM CCNT ; The cycle counter is ticking; Even if it is not used. JMP Main BT_Push: SETB PORTD,4 ; Light up LED1 CLRB PORTD,5 ; Turn off LED2 RJMP Next ; End Main ================================================================ =====

; Main ================================================================= ======== Main: SBIS PIND,6 ; If the button is pressed - transition RJMP BT_Push SETB PORTD,5 ; Let's light LED2 CLRB PORTD,4 ; Turn off LED1 Next: LDS R16,TCNT ; Load numbers into registers LDS R17,TCNT+1 CPI R16.0x12 ; Let's compare byte by byte. First byte BRCS NoMatch ; If it’s less, it means it didn’t hit. CPI R17.0x7A ; Second byte BRCS NoMatch ; If it’s less, it means it didn’t hit. ; If it matches, then we do the Match action: INVB PORTD,7,R16,R17 ; Inverted LED3; Now we need to reset the counter, otherwise during the same iteration of the main loop; we will get here more than once - the timer will not have time to reach 255 values; so that the number in the first two bytes of the counter changes and the condition is triggered. ; Of course, you can bypass this with an additional flag, but it’s easier to reset the counter :) CLR R16 ; We need zero CLI; Access to a multibyte variable; simultaneously from interruption and background; Atomic access is needed. Disable interrupts OUTU TCNT0,R16 ; Zero to the timer counter register STS TCNT, R16 ; Zero in the first byte of the counter in RAM STS TCNT+1,R16 ; Zero in the second byte of the counter in RAM STS TCNT+2,R16 ; Zero in the third byte of the counter in RAM STS TCNT+3,R16 ; Zero in the first byte of the counter in RAM SEI; Let's enable interrupts again. ; If it doesn't match, we don't do it :) NoMatch: NOP INCM CCNT ; The cycle counter is ticking; Even if it is not used. JMP Main BT_Push: SETB PORTD,4 ; Light up LED1 CLRB PORTD,5 ; Turn off LED2 RJMP Next ; End Main ================================================================ =====

This is what it looks like in action

And if we need to blink a second diode with a different period, then we can safely put another variable into the program, and in the timer interrupt handler we can increment two variables at once. Checking them one by one in the main program loop.

You can optimize the verification process a little more. Make it faster.

You just need to make the account not up, but down. Those. We load a number into a variable and start decrementing it in the interrupt. And there, in the handler, we check it for zero. If zero, then set a flag in memory. And our background program catches this flag and launches the action, simultaneously resetting the shutter speed.

But what if you need to be more precise? Well, there is only one option - to use event processing directly in the interrupt handler, and adjust the value in TCNT:TCNT0 each time so that the interruption occurs exactly at the right time.

The timer-counter is one of the most popular resources of the AVR microcontroller. Its main purpose is to count down specified time intervals. In addition, timer-counters can perform a number of additional functions, such as generating PWM signals, counting the duration and number of incoming pulses. For this purpose, there are special operating modes of the timer-counter.

Depending on the microcontroller model, the number of timers and the set of their functions may differ. For example, the Atmega16 microcontroller has three timer-counters - two 8-bit timer-counters T0 and T2, and one 16-bit timer-counter - T1. In this article, using the ATmega16 as an example, we will look at how to use the T0 timer-counter.

Pins used

Timer-counter T0 uses two pins of the ATmega16 microcontroller. Pin T0 (PB0) is an external clock input. It can be used, for example, to count pulses. Pin OC0 (PB3) is the output of the timer-counter comparison circuit. A square wave or PWM signal can be generated at this pin using a timer. It can also simply change its state when the comparison circuit is triggered, but we’ll talk about that later.


Pins T0 and OC0 are activated only with the appropriate timer settings; in the normal state these are general-purpose pins.

Timer-counter registers T0

Although it is boring, registers are something without which it is impossible to program microcontrollers, of course, if you are not sitting tightly on Arduino. So, timer T0 has three registers:

Counting register TCNT0,
- comparison register OCR0,
- configuration register TCCR0.

In addition, there are three more registers related to all three ATmega16 timers:

TIMSK configuration register,
- TIFR status register.
- special function register SFIOR

Let's start with the simplest.

This is an 8-bit counting register. When the timer is running, each clock pulse changes the value of TCNT0 by one. Depending on the operating mode of the timer, the counting register can either increase or decrease.
The TCNT0 register can be both read and written. The latter is used when you need to set its initial value. When the timer is running, changing the contents of TCNT0 is not recommended, as this will block the comparison circuit for one clock cycle.

This is an 8-bit comparison register. Its value is constantly compared with the counting register TCNT0,and if there is a match, the timer can perform some actions - cause an interrupt, change the state of the OC0 pin, etc. depending on the operating mode.

TCCR0 (Timer/Counter Control Register)


This is the T0 timer-counter configuration register and defines the timer clock source, the prescaler factor, the T0 timer-counter mode, and the behavior of the OC0 pin.Essentially the most important register.

Bits CS02, CS01, CS00 (Clock Select)- determine the clock frequency source for timer T0 and set the prescaler coefficient. All possible states are described in the table below.


As you can see, the timer-counter can be stopped, can be clocked from the internal frequency, and can also be clocked from the signal at the T0 pin.

Bits WGM10, WGM00 (Wave Generator Mode)- determine the operating mode of the timer-counter T0. There can be four of them - normal mode (normal), timer reset on coincidence (CTC), and two pulse width modulation modes (FastPWM and Phase Correct PWM). All possible values ​​are described in the table below.

We will analyze the modes in the code in more detail. Now all the nuances will still not be remembered.

Bits COM01, COM00 (Compare Match Output Mode)- determine the behavior of the OC0 output. If any of these bits are set to 1, then the OC0 pin ceases to function as a normal general-purpose pin and is connected to the T0 counter timer comparison circuit. However, it must still be configured as an output.
The behavior of the OC0 pin depends on the operating mode of the timer-counter T0. In normal and CTC modes, the OC0 pin behaves the same, but in pulse width modulation modes its behavior is different. Let’s not bother ourselves now with all these options and analyze the tables for each mode, let’s leave this for the practical part.

And the last bit of the TCCR0 register is the bit FOC0 (Force Output Compare).This bit is intended to force a change in the state of the OC0 pin. It only works for Normal and CTC modes. When the FOC0 bit is set to one, the output state changes according to the values ​​of the COM01, COM00 bits. The FOC0 bit does not cause an interrupt or reset the timer in CTC mode.

TIMSK (Timer/Counter Interrupt Mask Register)


A common register for all three ATmega16 timers, it contains interrupt enable flags. Timer T0 can cause interrupts when the counting register TCNT0 overflows and when the counting register coincides with the comparison register OCR0. Accordingly, two bits are reserved for the T0 timer in the TIMSK register - TOIE0 and OCIE0. The remaining bits refer to other timers.

TOIE0- The 0th bit value disables interruption due to an overflow event, 1 - enables it.
OCIE0- The 0th value disables interrupts due to a coincidence event, and 1 enables it.

Naturally, interrupts will be called only if the global interrupt enable bit is set - bit I of the SREG register.

TIFR (Timer/Counter0 Interrupt Flag Register)


A register common to all three timer-counters. Contains status flags that are set when events occur. For timer T0, this is the overflow of the counting register TCNT0 and the coincidence of the counting register with the comparison register OCR0.

If at these moments interrupts are enabled in the TIMSK register and the I bit is set, the microcontroller will call the corresponding handler.
The flags are automatically cleared when the interrupt handler starts. This can also be done programmatically by writing 1 to the corresponding flag.

TOV0- set to 1 when the counting register overflows.
OCF0- set to 1 when the counting register coincides with the comparison register

SFIOR (Special Function IO Register)


A beginner, in principle, may not even know about this register; one of its bits resets the 10-bit binary counter, which divides the input frequency for timer T0 and timer T1.

Reset occurs when the bit is set PSR10 (Prescaler Reset Timer/Counter1 and Timer/Counter0) per unit.

Conclusion




The ATMega16 MCU has three timers/counters - two 8-bit (Timer/Counter0, Timer/Counter2) and one 16-bit (Timer/Counter1). Each of them contains special registers, one of which is the counting register TCNTn (n is the number 0, 1 or 2). Each time the processor executes one instruction, the contents of this register are incremented by one (either every 8, 64, 256, or 1024 clock cycles). That's why it's called counting. In addition to it, there is also a comparison register OCRn (Output Compare Register), into which we can write any number ourselves. An 8-bit counter has 8-bit registers. As the program executes, the contents of TCNTn grow and at some point it will coincide with the contents of OCRn. Then (if special parameters are specified) in the TIFR interrupt flag register (Timer/Counter Interrupt Flag Register), one of the bits becomes equal to one and the processor, seeing the interrupt request, immediately breaks away from executing the endless loop and goes to service the timer interrupt. After this, the process is repeated.

Below is the timing diagram of the CTC (Clear Timer on Compare) mode. In this mode, the counting register is cleared when the contents of TCNTn and OCRn match, and the interrupt calling period changes accordingly.

This is far from the only mode of operation of the timer/counter. You don’t have to clear the counting register at the moment of a match, then this will be the pulse-width modulation generation mode, which we will consider in the next article. You can change the direction of counting, i.e. the contents of the counting register will decrease as the program runs. It is also possible to count not by the number of commands executed by the processor, but by the number of changes in the voltage level on the “leg” T0 or T1 (counter mode); you can automatically, without the participation of the processor, change the state of the OCn legs depending on the state of the timer. Timer/Counter1 can make comparisons on two channels at once - A or B.

To start the timer, you need to set the corresponding bits in the timer control register TCCRn (Timer/Counter Control Register), after which it immediately begins its work.

We will consider only some of the timer operating modes. If you need to work in a different mode, then read the Datasheet for ATMega16 - everything is written there in great detail in English, even examples of programs in C and assembler are given (no wonder it takes up 357 pages of printed text!).

Now let's move on to the buttons.

If we are going to use a small number of buttons (up to 9 pieces), then they should be connected between ground and the pins of any microcontroller port. In this case, you should make these pins inputs by setting the corresponding bits in the DDRx register and turning on the internal pull-up resistor by setting the bits in the PORTx register. In this case, the voltage on these “legs” will be 5 V. When the button is pressed, the MK input is closed to GND and the voltage on it drops to zero (or it may be the other way around - the MK output is shorted to ground in the depressed state). This changes the PINx register, which stores the current state of the port (unlike PORTx, which sets the state of the port when there is no load, i.e., before pressing any buttons). By periodically reading the PINx status, you can determine that a button is pressed.

ATTENTION! If the corresponding bit in the DDRx register is set to 1 for your button, then pressing the button well can lead to a small pyrotechnic effect - the appearance of smoke around the MK. Naturally, MK will have to be thrown into the trash after this...

Let's move on to the practical part. Create a new workspace and a new project in IAR with a name such as TimerButton. Set the project options as described in the previous article. Now let's type the following small code.

#include"iom16.h" void init_timer0( void) //Initialize timer/counter0( OCR0 = 255; //Contents of the comparison register //Set the timer operating mode TCCR0 = (1 void init_timer2( void) //Initialize timer/counter2( OCR2 = 255; TCCR2 = (1 //Set a match interrupt for it) void main( void) (DDRB = 255; init_timer0(); init_timer2(); while(1) { } } #pragma vector = TIMER2_COMP_vect //Timer interrupt 2 __interrupt void flashing() ( if((PORTB & 3) == 1) ( PORTB &= (0xFF // Disable pins PB0, PB1 PORTB |= 2; // Enable PB1 } else( PORTB &= (0xFF // Disable pins PB0, PB1 PORTB |= 1; // Enable PB0 } }

Let's see how it works. The init_timern functions set bits in the TCCRn, OCRn and TIMSK registers, and this method may seem strange or unfamiliar to some. We will have to explain first what the entry “(1

where a is the number whose binary representation needs to be shifted, and b indicates how many bits it needs to be shifted by. In this case, the loss of the value stored in a is possible (i.e., it is not always possible to restore from C what was in a). Let's look at an example:

What will end up in C after executing the line C = (22

2 in binary code will look like 00010110, and after shifting to the left by 3 bits we get C = 10110000.

Similarly, there is a shift to the right. Another example:

char C; … C = ((0xFF > 2);

First, the action in the inner brackets will be performed (0xFF is 255 in hexadecimal code), from 11111111 the result will be 11111100, then a shift to the right will occur and we will get C = 00111111. As we see, here two mutually inverse operations led to a different number, since we lost two bits. This would not happen if variable C were of type int, since int occupies 16 bits.

Now let's look at two more bit operators that are widely used in MK programming. These are the bitwise and (&) and bitwise or (|) operators. How they work, I think, will be clear from the examples:

Action: Result (in binary): C = 0; // C = 00000000 C = (1 // C = 00100101 C |= (1 // C = 00101101 C &= (0xF0 >> 2); // C = 00101100 C = (C & 4) | 3; // C = 00000111

I almost forgot! There is also “bitwise exclusive or” (^). It compares the corresponding bits in a number, and if they are the same, returns 0, otherwise one.

Let's return to our program. It says "(1

/* Timer/Counter 0 Control Register */ #define FOC0 7 #define WGM00 6 #define COM01 5 #define COM00 4 #define WGM01 3 #define CS02 2 #define CS01 1 #define CS00 0

When compiling the program, the WGM01 entry is simply replaced with the number 3, and the result is a correct entry. WGM01 is called a macro and, unlike a variable, it does not take up space in memory (except in the programmer’s memory :-).

If you look now at the Datasheet, it will be easy to see that WGM01 is the name of the third bit in the TCCR0 register. The same applies to the remaining bits of this register. This coincidence is not accidental and applies to all MK registers (or almost all). That is, by writing “(1

Total, line

means that the CTC mode is turned on, when timer0 is triggered, the state of the “leg” OS0 (aka PB3) changes, the contents of the counter increases every 1024 clock cycles.

Similarly for timer2: TCCR2 = (1

The TIMSK register (Timer/counter Interrupt MaSK register) sets the interrupt mode. We wrote

which means interrupting timer2 when TCNT2 and OCR2 match. The very last function is the actual Timer2 Match Interrupt function. Interrupts are declared as follows:

#pragma vector= VECTOR __interrupt TYPE NAME()

where VECTOR is the interrupt vector macro (meaning simply a number characterizing this interrupt); These macros are listed in order of decreasing priority in the iom16.h file. TYPE is the type of value returned by the function, in our case void (nothing). NAME – a custom name for this function. With interruptions, we will still have time to work on it in the future.

When performing our function, the LEDs connected to PB0 and PB1 should blink in turn. Apparently, the frequency is 11059200/(256*1024) = 42 Hz. It's quick, but will be noticeable to the naked eye. By the way, the use of timers makes it possible to count precise time intervals that do not depend on the complexity of your program and the order in which it is executed (but if you have no more than one interrupt).

So, save the file as “TimerDebug.c”, add it to the project, compile it, flash the MK. What do we see? The LED connected to pin PB3 will blink actively, but there will be no changes at PB0 and PB1. What's the matter? Is something really wrong?

To find out, we will have to debug our program. Since IAR does not have a Debugger, you will have to use AVR Studio. This development environment can be downloaded from the manufacturer's website http://atmel.com. I don't think there should be any problems with its installation. Before starting AVR Studio, select Debug mode in IAR and create a debug cof file (all project options must be set as described in the previous article).

Having opened AVR Studio, we will see a welcome window in which we will select “Open”. Now we go into the folder with the project, there in Debug\Exe, select “TimerDebug.cof” there, create a project where they suggest, select the ATMega16 device and the Simulator debugging mode. After this, if everything was done correctly, the debugging process immediately begins

The debugging environment here is very convenient, because... allows you to view the contents of all MK registers, as well as manually set values ​​for them with mouse clicks. For example, if you set the interrupt flag in the TIFR register in bit 7 (under the black square in TIMSK), then the next step of the program (pressing F10 or F11) should be interrupt processing (the flag will be set automatically if the TCNT2 and OCR2 registers match). But to our surprise, there will be no interruption!

The question arises: why?

Let's open the CPU register, SREG. This register determines the operation of the processor, and specifically its seventh bit (I-bit, Interrupt bit) is responsible for processing all interrupts in the MK. We don't have it installed. As soon as you set it, the interrupt will immediately be executed (if the seventh bit in TIFR is set at the same time).

You can notice one interesting feature: as soon as the processor goes into interrupt processing, this bit (the interrupt processing enable flag) is cleared, and when exiting the interrupt function it is automatically set again. This does not allow the processor, without executing one interrupt, to grab another (after all, it navigates the program in exactly this way - by flags).

This means that you need to add a line of code to set this bit to one. We will add it to the init_timer2 function. You will get the following:

void init_timer2( void) ( SREG |= (1 //Added this line OCR2 = 255; TCCR2 = (1

Now, having selected the Release configuration and flashed the MK by pressing F7 and launching AVReal32.exe, we will be happy to see that everything works as it should.

Comment: When debugging a program, you should reduce the timer intervals if they are too long, because during debugging in AVR Studio the program runs thousands of times slower than inside the MK and you will not wait for the timer to fire. In general, debugging is completely similar to that in other programming systems, such as Visual C++.

Now, having learned how to debug programs, let’s create a new file in IAR (and save the old one and delete it from the project) and type the following code:

#include"iom16.h" long unsigned int counter = 0; //Counter for forming time intervals unsigned char B0Pressed = 0; //The state of button0 is stored here (0 - not pressed, 1 - pressed) unsigned char B1Pressed = 0; //The state of button1 is stored here (0 - not pressed, 1 - pressed) //Initialize timer2 //It is necessary to increase counter every 11059 clock cycles (1 ms). We get it every 1.001175 ms void init_timer2() ( OCR2 = 173; TCCR2 = (1 //Initializing I/O ports init_io_ports() ( DDRA =(1//forming a delay in Pause_ms milliseconds void delay( long unsigned int Pause_ms) ( counter = 0; while(counter void main() ( SREG |= (1 //Enable interrupts init_timer2(); //Turn on timer2 every 64 ticks, count up to 173 init_io_ports(); //Enable I/O ports while(1) { //Processing button 0 if(B0Pressed == 1) { //increases PORTB, waits for release PORTB++; B0Pressed = 0; while((PINC & (1 else ( if((PINC & (1 //Fixes pressing ( delay(50); if((PINC & (1 //Checks pressing ( B0Pressed = 1; } } } //Processing button 1 if(B1Pressed == 1) //If the button is clicked, { //decrements PORTB, waits for release PORTB--; B1Pressed = 0; while((PINC & (1 else ( if((PINC & (1 //Fixes pressing ( delay(200); //Eliminating "key bounce" if((PINC & (1 //Checks pressing ( B1Pressed = 1; //Sets the "button pressed" flag } } } } } //Interrupt by timer 2, so the counter increases #pragma vector= TIMER2_COMP_vect __interrupt void inc_delay_counter() ( counter++; )

First, I suggest taking a ready-made firmware file (files for the article, the Release folder, the TimerButton.hex file, or compiling this text) and writing it into the MK. Then remove the firmware cable, connect the buttons to PC0 and PC1 and try to press them. We will see that when you press one of the buttons, the PORTB register increases (the LEDs light up), and when you press the other, it decreases. If it doesn't work, try pressing one button while holding the other - it will work. The fact is that I connected the buttons in the following way: when you press the button, the MK output “dangles” in the air, and when released it shorts to the ground. If you connected the buttons differently, you will only have to slightly modernize the program.

Let's look at the code. Here, working with the timer is organized somewhat differently. It fires every 11072 clock cycles (that is, every 1.001175 ms) and increments the counter variable. There is also a function delay(long unsigned int Pause_ms), which takes the number of milliseconds Pause_ms as a parameter, resets the counter and waits for the counter to reach the Pause_ms value, after which the MK continues to operate. Thus, by writing delay(1500), we will create a delay in the program of 1.5 seconds. This is very convenient for forming time intervals.

Everything seems to be clear with the timer. But what is it used for? Consider the infinite loop while(1) in main(). This loop checks the state of the buttons by analyzing the contents of the PINB register. Why is there a 50 ms delay there? This is the elimination of the so-called. "key chatter" The fact is that when you press the button, one contact hits another, and since the contacts are metal, this impact is elastic. The contacts, springing, close and open several times, despite the fact that the finger made only one press. This leads to the MK recording several clicks. Let's look at a graph of the voltage at the PC0 output versus time. It might look like this:

Point A is the moment the button is pressed. It can be fixed by MK. Then there are several short circuits and open circuits (there may not be any, or there may be 12 of them - this phenomenon can be considered random). At point B, the contact is already securely fixed. Between A and B there is an average of about 10 ms. Finally, at point D, an opening occurs. How to get rid of this unpleasant phenomenon? It turns out to be very simple. You need to record the moment the button is pressed (point A), after some time, for example, 50 ms (point C), check that the button is really pressed, take the action corresponding to this button and wait for the moment it is released (point D). That is, you need to make a pause from A to C, such that all the “blink” is inside this pause. Now try to remove the line that creates the delay, compile the program and stitch it into the MK. By simply pressing buttons, you can easily make sure that all this “torment” was not in vain.

But what to do if you need to connect, say, 40 buttons to the MK? After all, it only has 32 pins. It would seem that there is no way. It's actually possible. In this case, an algorithm called gating is used. To do this, you need to connect the buttons in the form of a matrix, as shown in the figure (the figure is taken from Morton’s book “MK AVR, an introductory course,” where it is written about AVR programming in assembler).

When applied to the output PB0 log. 1 (+5V), and to pins PB1 and PB2 log. 0 allows processing of buttons 1, 4 and 7. After this, the status of each of them can be found out by checking the voltage at one of the pins PB3..PB5. Thus, applying sequentially to the pins PB0..PB2 log. 1, the status of all buttons can be determined. It is clear that pins PB0..PB2 should be outputs, and PB0..PB2 inputs. To determine how many pins are required for an array of X buttons, you need to find a pair of X factors whose sum is the smallest (for our case with 40 buttons, these will be numbers 5 and 8). This means that up to 256 buttons can be connected to one MK (and even more with the use of decoders, but more on decoders later). It is better to make fewer pins outputs and more pins inputs. In this case, scanning all rows of the matrix will take less time. This connection method (strobe) is not unique to buttons. There you can connect a wide variety of devices, from LED matrices to flash memory chips.

© Kiselev Roman
June 2007


Timers and counters for AVR microcontrollers (real time clock). AVR Lesson 7

When I was still starting to study microcontrollers, I wanted to do so. Honestly, I wanted to try to turn on the TV only from 7 to 8 o'clock, and the rest of the time it should have been turned off. I made the device, but never used it...

All AVR microcontrollers have several built-in timers. They can also be divided into general-purpose timers and a watchdog timer, which is designed to reboot the MK when it freezes.

General purpose timers can:

  • Clock from external clock quartz at 32768 hertz
  • Count different time intervals
  • Count external pulses in counter mode
  • Generate a PWM signal at certain MK pins
  • Generate interrupts for some event, for example, when there is an overflow

Counter timers can be clocked from an internal clock generator and from a counting input. Let's look at the functionality of timer-counter 1 in the atmega8 microcontroller. Launch CodeVision AVR, create a new project and agree to the offer to launch Code WizardAVR

Let's use timer2 as an example to implement a real-time clock with output to an LCD display; for this we set the timer as shown in the screenshot

here we set the external clock source for the timer, as an external source we will use a clock quartz at 32768 hertz, then we will set the prescaler to 128, that is, the timer will operate at a frequency of 32768/128=256, and the counting register is 8-bit (maximum number 255), it turns out that it will overflow once per second, then we check the box next to Overflow interrupt and click on file->Generate, save and exit.

Code Wizard generated the following code:

#include // Timer2 overflow interrupt service routine interrupt void timer2_ovf_isr(void) ( ) void main(void) ( // Input/Output Ports initialization // Port B initialization PORTB=0x00; DDRB=0x00; // Port C initialization PORTC=0x00; DDRC=0x00; // Port D initialization PORTD=0x00; // Timer/Counter 0 initialization // Clock source: System Clock // Timer 0 Stopped TCCR0=0x00; /Counter 1 initialization // Clock source: System Clock // Clock value: 125,000 kHz // Mode: Fast PWM top=00FFh // OC1A output: Discon. // OC1B output: Discon. // Noise Canceler: Off // Input. Capture on Falling Edge // Timer1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: Off // Compare B Match Interrupt: Off TCCR1B=0x0A; ICR1H=0x00; OCR1AH=0x00; OCR1BH=0x00; // Timer/Counter 2 initialization // Clock value: PCK2/128 // Mode: Normal top=FFh // OC2 output: Disconnected ASSR=0x08; TCCR2=0x05; TCNT2=0x00; OCR2=0x00; // External Interrupt(s) initialization // INT0: Off // INT1: Off MCUCR=0x00; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x40; // Analog Comparator initialization // Analog Comparator: Off // Analog Comparator Input Capture by Timer/Counter 1: Off ACSR=0x80; SFIOR=0x00; // Global enable interrupts #asm("sei") while (1) ( ); )

#include #include // Alphanumeric LCD Module functions #asm .equ __lcd_port=0x12 ;PORTD #endasm #include unsigned char second=0; //variable for storing seconds unsigned char minute=0; //variable for storing minutes unsigned char hour=0; //variable for storing hours char lcd_buffer; //variable buffer for display output // Timer2 overflow interrupt service routine interrupt void timer2_ovf_isr(void) ( if (++second==59) //increase the number of seconds by 1 and check the equality 59 (second = 0; if (+ +minute==59) (minute = 0; if (++hour==59) ( hour = 0; ) ) lcd_clear(); // clear the display before output lcd_gotoxy(0,0); point x=0 y=0 sprintf(lcd_buffer,"%i:%i:%i",hour,minute,second); // create a line for output lcd_puts(lcd_buffer); // display the line ) void main( void) ( // Timer/Counter 2 initialization // Clock source: TOSC1 pin // Clock value: PCK2/128 // Mode: Normal top=FFh // OC2 output: Disconnected ASSR=0x08; TCCR2=0x05; TCNT2=0x00 ; OCR2=0x00; // Timer(s)/Counter(s) initialization TIMSK=0x40; // LCD module initialization lcd_init(16); ) ( )

The program is ready, now let's create a diagram in Proteus