This is an old revision of the document!
This lab covers the topics of PWM, H-bridge drivers and reading encoders using external interrupts. For more in-depth knowledge about the capabilities and the timing peripherals and how to program them you can find more information in the ATmega324 datasheet (Datasheet ATmega324).
PWM (Pulse Width Modulation) or PDM (Pulse Duration Modulation) is a modulation technique used to reduce the average power delivered by an electrical signal to a load by chopping it into discrete parts. The average value of a signal is dependent on the maximum and minimum amplitude and on the duration of the pulses of minimum and maximum amplitude.
In order to use PWM and prior to program our microcontroller to generate PWM signals, we first need to understand the basics of PWM and how to use it in different circuits.
The output signal (or the Pulse Width Modulated signal) will be generated by using electronic switches (such as Bipolar Transistors, MOSFET Transistors, IGBT) that connects a positive voltage power supply (e.g. a battery or a fixed voltage output) for a precise amount of time (T1) to the output, afterwards it connects a negative voltage (or the GND for unipolar PWM) the output from the power supply for another amount of time (T2), and than, the cycle repeats.
In the following schematic, we can see that Q1 connects the output to the positive voltage supply (Vmax) and Q2 connects the ouput to the negative voltage supply (Vmin). Q1 and Q2 are controlled by a more complex circuit (transistor driving circuit), based on the signals given by the microcontroller (MCU). The transistor driving circuit is sometimes necessary because of the different voltage levels of the MCU and the voltages of the power supplies.
As we can observe, in one cycle (of duration T=T1+T2) the signal is switched between Vmax and Vmin.
The average voltage of the signal (measured with respect to GND) can be calculated by computing the integral over time. V_1
\begin{equation} V_{avg}=\frac{1}{T}\int_{0}^{T} Vout(t) dt \end{equation}
Considering for one cycle, that from 0 to T1 the output is Vmax and from T1 to T (interval equal to T2) the output is Vmin than the average voltage will be computes as follows:
\begin{equation} V_{avg}=\frac{T1}{T}V_{max} + \frac{T2}{T} V_{min} \end{equation}
In electronics, the ratio between T1 (the duration of the positive pulse) and the period T is called the duty cycle. The duty cycles tells us how much time, compared to the total period, the positive pulse duration is.
\begin{equation} d=\frac{T1}{T} \end{equation}
The duty cycle can be measured as a (percentage) % and it resembles the fraction of the positive pulse (T1) compared the total period (T).
\begin{equation} d_{\%}=\frac{T1}{T}100 \end{equation}
Considering the definition of the duty cycle, the average voltage is:
\begin{equation} V_{avg}=d\cdot V_{max} + (1-d)\cdot V_{min} \end{equation}
For the signal showed in the previous figure, we can observe that T1 is 7.5ms and T2 is 2.5ms (T=10ms), Vmax=10V, Vmin= -10V. That leads us to $d=0.75 or d\%=75\%$
Thus, our output average voltage will be:
\begin{equation} V_{avg}=0.75\cdot 10 + 0.25\cdot (-10)= 5V \end{equation} For an unipolar signal (the signal is switched between Vmax and 0 (GND) ) the average voltage will be computed as:
\begin{equation} V_{avg}=\frac{T1}{T}V_{max}=d\cdot V_{max} \end{equation}
Driving LEDs is done the same way as for simple digital outputs, as explained in our previous labs. If the LED is a high power device (more than 25mW LED), we will need a controllable LED driver (usually based on a switchable current supply).
The LED can be connected from the output of the MCU to GND with a resistor and it will light up when the MCU pin is driven HIGH. The resistor is used to limit the current through the LED.
The LED can be connected from the power supply to the MCU pin and it will light up when the MCU pin is driven LOW (the current will flow from Vcc to the MCU pin).
Encoders are electronic or electro-mechanic devices used to codify the relative movement (rotation) of a shaft. They generate two pulses (A and B) that are shifted 90 degrees apart that codify the speed of the movement and the direction of the movement.
The encoders are usually characterized by the number of pulses/complete revolution and the type of circuitry for the output signals (PNP, NPN, PUSH-PULL stage).
For example, let's say that we have an encoder with a Push-Pull stage of 5V (it can pull the output to 5V and GND and generate both states - HIGH and LOW) that generates a 4 pulses per complete revolution.
The steps for working with interrupts are as follows:
Now let's see how this looks in code for Pin Change Interrupts:
#include <avr/interrupt.h> void init() { // disable global interrupts cli(); // configure pin change interrupt vector PCICR |= (1 << PCIE1); // enable the pin change interrupt, set PCIE1 to enable PCMSK1 scan // enable pin change interrupt PCMSK1 |= (1 << PCINT9); // Turns on PCINT9 (PB1) // enable global interrupts sei(); } // define the pin change interrupts ISR ISR(PCINT1_vect) { // interrupt code if ((PINB & (1 << PB1)) == 0) { // interrupt generated by PB1 } }
Timers are used to count fixed time intervals without busy-waiting (delay). They are defined by the counter register TCNT, a prescaler and some interrupt vectors that can be defined to trigger when the counter reaches a preset threshold (e.g OCRnA, OCRnB).
Timers can be configured for multiple use cases:
The Atmega324 has 3 timers: 8 bit Timer0/2 and 16 bit Timer1. In this lab, we will discuss the configuration and applications for internal timers.
A timer can generate the following interrupts:
Speed of the counting (and interrupts) can be controlled by the prescaler, mode of operation and counter limits as follows:
// calculate the frequency of the interrupt (f) from the timer configuration and clock speed (f_clk) f_int = f_clk / (prescaler * (tc + 1)) // calculate the target timer count (tc) for the required interrupt frequency tc = f_clk / (prescaler * f_int) - 1
As we know, all the peripherals, including Timers are connected to the CPU by some registers for configuration and input-output:
Timer modes:
TCNTn
reaches the compare threshold OCRnA
/ICRn
Now let's see how this looks in code:
#include <avr/interrupt.h> void init_timer1() { // initialize the timer counter TCNT1 = 0; // configure the threshold OCR1A = 10000; // enable compare interrupt A TIMSK1 = (1 << OCIE1A); // configure mode of operation e.g. CTC with OCR1A TCCR1B = (1 << WGM12); // set the prescaler e.g 256 TCCR1B |= (1 << CS12); } ISR(TIMER1_COMPA_vect) { // interrupt code }
Allright, now we are interested in counting precise time intervals and doing repeated actions. For example, we want to count seconds and display them on a 7-segment display. Check out Laboratorul 2: Întreruperi, Timere and (Datasheet ATmega324) for configuration options.
7-segment LED displays are used in many applications as front panel numeric indicators. The most common applications are calculators, digital clocks, microwave ovens, electronic lab equipment like function generators and frequency counters. It's also common to have 7-segment displays that have a dot that can be enabled after the digit.
A 7-segment LED display is an arrangement of LED bars (a, b, c, d, e, f, g, dp) that can be powered individually to display digits (and some characters too). The configuration can be either common anode or common cathode:
The following lookup table may be useful for programming a 7-segment display driver to show some numbers:
Digit | hex | a | b | c | d | e | f | g | |
---|---|---|---|---|---|---|---|---|---|
0 | 0x7e | 1 | 1 | 1 | 1 | 1 | 1 | 0 | |
1 | 0x30 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | |
2 | 0x6d | 1 | 1 | 0 | 1 | 1 | 0 | 1 | |
3 | 0x79 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | |
4 | 0x33 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | |
5 | 0x5b | 1 | 0 | 1 | 1 | 0 | 1 | 1 | |
6 | 0x5f | 1 | 0 | 1 | 1 | 1 | 1 | 1 | |
7 | 0x70 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | |
8 | 0x7f | 1 | 1 | 1 | 1 | 1 | 1 | 1 | |
9 | 0x7b | 1 | 1 | 1 | 1 | 0 | 1 | 1 |
1. (1p) Now we are dealing with timers, so accurate timing is important.
In the previous lab, the microcontroller was configured (by default) to use the internal 8MHz oscillator on the Atmega for clock generation, with a clock divider (CLKDIV8
fuse bit) set by default, making for a 1MHz actual clock. However, if we want higher precision/more speed we can use an external oscillator (crystal/quartz) with frequencies up to 20MHz (with Vcc = 5V) for the Atmega324.
For this, we have to set the fuse bits as shown in the figure below. Right click on the microcontroller and select Edit Component to open the configuration editor and change the fuse bits to use external oscillator (CKSEL
Fuses) and disable the clock divider (clear 'CLKDIV8') fuse bit. Configure the clock speed at 12MHz (for convenience, to match the clock on the PM boards and use the same Makefile):
2. (1p) Add a quartz crystal from the parts list and connect it to the XTAL1
and XTAL2
pins on the Atmega324. Right click on the crystal to open the configuration editor and set the frequency to 12MHz. Note: for physical designs, you need additional 22pF capacitors on each pin (with the other sides connected to GND).
3. (2p) Connect a push button to PD3
and a generic LED to PD4
. Pick a resistor for the LED to draw less than 10mA. The LED will show the state (enabled/disabled) of the 7-segment digital counter that we'll build next. When the user pushes the button, the program will enable/disable the counter instantly (use interrupts on PD3
and the PCINT ISR on PORTD
to detect the button transition).
4. (2p) Configure Timer1 to generate interrupts every second. Now, use a software counter (aka. variable) to count seconds (0-9 for 5.1 and 0-99 for 5.2). Now, add another LED to PD2
to check if the counter is working. Make the LED toggle on each timer interrupt. Watch the LED blinking. What is the frequency of the LED blinking?
volatile
keyword (e.g. volatile uint8_t counter;) for variables that you use in interrupts. Also, try to use standard types from stdlib.h
/stdint.h
(e.g. int8_t, uint8_t, uint16_t, uint32_t).
5. (2p/3p) Place and connect the components on the schematic for building the 7-segment display
PORTA
0..6 through a resistor network (single resistors work too) and the cathode to ground. Write a program that counts seconds from 0 to 9 and then starts over.PORTA
0..6 through a resistor network (single resistors work too). Pick a transistor (2n2222A is a good choice for switching) and place two of them to drive the cathodes. Connect the base of each transistor to a microcontroller pin with a resistor of 1k in between. Let's use PA7
and PC2
. Write a program that counts seconds from 0 to 99 and then starts over.
Check out the 7-segment display counter in action:
6. (2p/3p) In the main loop, get the digits from the counter (variable) and use them to drive the two 7-segment displays. Power the segments as shown in the truth table and show each of the two digits with a small delay between them. You can actually use the _delay_ms function in the main loop and this will not affect your timing (counting is done with timer interrupts). Note: In simulation, you will have to use at least 100 ms and you won't be able to see them both at the same time.
PA7
pin when writing the binary combination of segments on PORTA
as that pin is being used for another task (enabling the digits)