Notes before proceeding:
In this exercise, we will explore the functionality of PIC microcontrollers in interfacing with simple input/output devices such as pushbuttons and light-emitting diodes (LEDs). From the development board, general-purpose input/output (GPIO) pins can be considered the simplest of peripherals. All I/O ports have the following registers directly associated with the configuration of the port, where x
is a letter that denotes the particular I/O port. Each I/O pin in the development board has an associated bit in the TRIS, PORT, ANSEL, and LAT registers.
TRISx |
Port 'x' Data Direction Control Register
The TRISx register control bits determine whether each pin associated with the I/O port is an input or an output. If the TRIS bit for an I/O pin is a `1`, then the pin is an input. if the TRIS bit for an I/O pin is a `0`, then the pin is configured for an output. 1 - input 0 - output |
---|---|
PORTx |
I/O Port Register
Data on an I/O pin is accecssed via a PORTx register. A read of the PORTx register reads the value of the I/O pin, while a write to the PORTx register writes the value to the port data latxch. This will also be reflected on the PORTx pins if the TRISx is configred as an output and the mutliplexed peripherals (if any) are disabled. |
LATx |
Port 'x' Data Latch Register
The LATx register associated with an I/O pin eliminates the problems that could occur with read-modify-write instructions. A read of the LATx register returns the values held in the port output latches instead of the values of the I/O pins.A read-modify-write operation on the LATx register, associated with an I/O port, avoids the possibility of writing the input pin values into the port latches. A write to the LATx register has the same effect as a write to the PORTx register. |
ANSELx |
Port 'x' Analog Select Register
The ANSELx register controls the operation of the analog port pins. The port pins that are to function as analog inputs must have their corresponding ANSELx and TRISx bits set. For a pin to be configured as digital I/O, the ANSEL bit must be set as `0`. For a pin to be an analog I/O, the ANSEL bit must be set as `1`. |
Rule of Thumb: READ on PORTx
, WRITE on LATx
PORTx
reads the voltage (and consecutively, the logic value) seen at the I/O port. Note that the value read will only ever be '1' or '0' depending on if the voltage input is higher or lower than the internally set threshold.PORTx
register is not meant to be written on.LATx
reads the last written value onto the port, instead of the actual values on the pins. Depending on how fast the internal workings of the port are, this may not accurately be the current value on the port.LATx
sets the value on the latch register of the associated port. This allows the port to maintain that value until overwritten by the program.The PIC32MM0064GPL036 microcontroller has 3 GPIO ports. The available pins per port are shown below. See p.7 of the given PIC32MM family datasheet for the pin mapping.
PORTA[9]
, PORTA[4:0]
PORTB[15:0]
PORTC[9:8]
, PORTC[3:0]
Each bit of these ports can act as an input or output, depending on the TRISx
value. If TRISB[7]
is 0
while the rest of the TRISB
bits are 1
, then only RB7
is an output pin. If TRISA
is 0x000C
then only RA1
and RA3
are inputs while the rest of the pins of port A are outputs.
In the given development board, the general-purpose LEDs LED1 and LED2 are already connected to pins RA0
and RC9
. On the other hand, the pushbuttons S1 and S2 are connected to pins RB7
and RB13
, respectively. Shown below are the exact connections:
There are two ways to use digital outputs: buffered and open-drain. Buffered digital outputs are the more intuitive option where if the associated LATx
bit is 1
, the output pin is driven to , usually . Otherwise, if the associated LATx bit is 0
, the output pin is driven to , usually or GND.
In open-drain mode, the output pin acts as an open switch if the associated LATx
bit is 1
. Thus, if an external pull-up resistor is used, as shown in the left schematic below, then the voltage output of the pin can be increased up to the device limit of . Otherwise, the output pin will be pulled to GND by the activated switch if the associated LATx
bit is 0
. To enable open-drain mode for any GPIO pin, the corresponding bits in the ODCx
register needs to be set to 1
.
The Logic Analyzer is a feature of the MPLAB X IDE simulator that acts like a real logic analyzer, displaying the digital levels of all the selected pins during a selected period. It is useful for checking the behavior of functions.
To use the Logic Analyzer, do the following:
For the onboard LEDs, we can see back in Figure 1 that we just need to drive the corresponding pin to a high enough value to turn them on. We should be able to do that by configuring the connected GPIO pins as outputs and setting the output value to 1. To achieve this for LED1, we can put the following in our code:
Add a breakpoint in the line that sets the LATA0
. Then, debug the project by running the Debugger tool. When the debugger reached the breakpoint, click on the Step Into command once. You should see something similar to the figure below.
As you can see, the pin is driven to which is . If you continue to run the debugger tool, you'll see that the pin will continue to be at that voltage.
If you already have your own development board, you can also try to program your device with this code. The LED1 should light up. If it didn't work, and you are sure that you typed in the right commands, make sure your PIC is properly connected. You can try re-uploading your code again after disconnecting and reconnecting your cable. If it still doesn't work, contact your lab handler immediately.
To get a blinking LED, we need to turn it off and then on again in a loop. If we are using C, we should have something like this:
Did that work? Well it kinda works as shown in Figure 8 but it blinks too fast for any normal person to notice (you might notice that LED is a bit dimmer though). It is blinking too fast because our output port values change as soon as the commands are issued.
If our PIC executes commands at , that means that the LED is toggled every which is practically unnoticeble to the naked eye. To make things more interesting, let's try slowing that down with a delay:
We should now see that our LED is blinking at a steady rate, just in time for Christmas :)
The digital input pins of the PIC32, when configured correctly, are connected to Schmitt Triggers which is what is read through the PORTx
registers. A regular comparator outputs a digital high signal once its input reaches a certain threshold and outputs a digital low signal otherwise. A Schmitt Trigger is a modified comparator where there are two thresholds depending on the current output. The additional threshold makes the port less susceptible to noise as seen in the right plot in Figure 3. You can find on the datasheet the specific threshold values for the PIC32 input pins in the datasheet.
Getting both a high and a low voltage input to a single pin might make you think that two pushbuttons might be needed. Fortunately, there is a way to do this with just a single pushbutton through the use of internal pull-up resistors. Shown in Figure 2 is a diagram describing the configuration of a digital input configured with an internal pull-up resistor to 3.3V. If the connected switch is open, the input pin will read as the pull-up voltage of 3.3V. This in turn represents a logic value of HIGH. Closing the switch would in turn represent a logic value of LOW since there will now be a path to ground.
For onboard switches such as the ones in Figure 1, notice that for every pin connected to a pushbutton, there is already an external pull-up resistor to pull up the voltage to . Again, if the switch is open, there would be no path to the ground, hence the pin voltage will be pulled up to 3.3V. This in turn would mark as a logic HIGH in our microcontroller. Similarly, a closed switch would short the two terminals of the pushbutton, resulting in a logic LOW in our microcontroller.
Previously we used the GPIOs as output. In this section, we will now use our GPIO module as output, so that our microcontroller can be interfaced with devices such as pushbuttons. To start, here is a sample polling program for a single pushbutton:
First, we set the appropriate GPIO configurations for our pushbutton and LED. For simplicity, we set all of the ports of PortA and PortB to be digital by setting the register values to 0. The internal pull-up of RB7 was also configured so that when the normally-open pushbutton is not pressed, the pin voltage will be high enough to register as HIGH.
Remember from the previous sections that if a push-button is pressed, the switch is closed. Hence, the microcontroller will read a voltage level of 0V. Simply put, the program above continuously checks if PB1 is pressed. If a press is detected, the LED turns ON. This method of waiting for an input change is what we call "polling".
Stimulus is the input to the simulator, i.e., data generated to exercise the response of simulation to external signals. Stimulus in the simulator can be either asynchronous or synchronous. Asynchronous is generally used to refer to interrupts that may occur at any time during processor execution while synchronous means that the event is tied to an operation that occurs during the execution of the code. For this example, we will be using Asynchronous Stimulus which is data generated to simulate external inputs to a simulator device.
To show the Stimulus window, go to Window > Simulator > Stimulus. You should see the Stimulus window show at the lower section of the IDE. Select the Asynchronous tab. Add a row and select RB7
for the Pin and Set High as the Action. Then, add another row and also select RB7
for the Pin but this time, select Set Low as the Action.
Now, debug the sample pushbutton polling code. Then, fire the Set Low stimulus followed by the Set High stimulus. You should see the value of RA0
to correspond to the stimuli we fired using the Stimulus tool (i.e., RA0
became after we fired a stimulus that set RB7
to low and became after we fired a stimulus that set RB7
to high). The response should be something similar to Figure 12. To better follow these changes, you can use breakpoints and the Step Into command while debugging.
Pushbuttons often generate spurious open/close transitions upon pressing. These may be due to mechanical and physical issues which the controller may read as multiple presses in a very short program. Instead of detecting a simple change in the pin state, we use what is called a contact bounce period in which we first wait for the read waveform to stabilize for some time.
For more precise delays, we can utilize the following function prototypes:
Previously, we have used polling to get inputs from the switches on our boards. However, you may notice that during that time we are waiting for the input, the microcontroller cannot do anything else. If we ever wanted to have an LED that is blinking while waiting for the input we would probably need a not-so-simple delay loop to be implemented properly. Fortunately, we can go around this by using interrupts.
Interrupts are asynchronous (i.e. can happen anytime) events that disrupt the normal flow of your program. This allows the microcontroller to focus on a key task and attend to these events (e.g. pressing a button) as they come without needing to wait for them. Kevin Lynch provides us with an overview of interrupts in the PIC32 in this video (4:30).
Each interrupt source is defined by three groups of SFRs. Our friend Kevin Lynch also has a video on this (6:22) but below is a summary of what these SFRs do:
IECx |
Interrupt Enable Control
Setting the bits here to 1 will enable the interrupts associated with them. If an enabled interrupt occurs the Interrupt Service Routine (ISR) is executed by the microcontroller. |
|
---|---|---|
IFSx |
Interrupt Flag Status
Reading a value of 1 from these registers mean that the associated interrupt has occurred. While the flag of an interrupt is 1, succeeding occurrences of the interrupt will go unnoticed. Thus, it is good practice to clear the interrupt flag after the interrupt has been serviced. |
|
IPCy |
Interrupt Priority Control
These registers contain bits regarding the priority of the associated interrupt. For each IPC register there are up to 4 sets of priority and subpriority bits. |
|
IPz |
Interrupt Priority Bits
Each interrupt is given a priority so that the microcontroller knows which to address first if multiple occur at the same time. An interrupt that has occurred but is not yet addressed because another interrupt is being serviced is called a masked interrupt. With 3 bits associated to each interrupt, there are a total of 7 different priority levels. It is often considered that a priority level 0 is functionally disabled since the interrupt will always be masked by the main program.
|
|
ISz |
Interrupt Subriority Bits
For some microcontrollers, interrupts of the same priority level usually share some settings and special configurations to speed up configuration. To add some differentiation between same-level interrupts subpriority bits are used. |
Note that there are 2 registers each for the enable and control bits and an interrupt would be usually associated with the same register number. On the other hand, there is a total of 11 priority registers which, more often than not, is a different register number for the same interrupt. Hence, the use of 'x' an 'y'. You may check the datasheet or reference manual for the exact mapping of each interrupt source to their bit locations.
An ISR is the function that is automatically called if an enabled interrupt occurs. Because of their special functionality, they are placed in a specific part of memory pointed at by the Interrupt Vector. The PIC32MM can choose to use a single interrupt vector in Single-Vector Mode* (SVM) or use different vectors in Multi-Vector Mode (MVM) for each interrupt source.
In MVM, the XC32 compiler automatically puts the ISR in the appropriate location in memory as long as you define it correctly:
where:
void
- The leftmost one is the return type of the function as it is required in C++ syntax. The last one at the end of line 1 is optional as it just says that the function does not take in any arguments.
__ISR
- Tells the compiler that an ISR is to be written. Note the double '_' in the beginning and you need to include sys/attribs.h
to avoid any compiler errors.
_INTERRUPT_VECTOR_NAME
- Change this to the XC32 Vector Name associated with the interrupt as seen in table 7-2 of the PIC32MM datasheet. The Vector Number from the same table can also be used.
n
- This should be replaced by the interrupt priority level which should be the same as what is stored in the corresponding IPz bits.
YYYY
- Replaced by one of two choices of how context save and restore is done for the ISR:
SOFT
- Use the standard software context save and restore.SRS
- Use the Shadow Register Set, a faster way to do context save and restore but only configured priority levels can get to use this.ISR_NAME
- The name of the function itself and could be anything but it is recommended that it should be at least related to what it does.
The following are a few examples of ISR definitions:
Properly harnessing the power of interrupts may sound like a daunting task. Fortunately, one of our friends has this video on how you can properly configure and use interrupts on the PIC32 in 7 easy steps (3:47). Listed below are these 7 steps and I have also added my own 0th step which totally did not cost me 2 hours of debugging:
IECx
bits.With the XC32 compiler, we have the following example of the configuration of a Change Notice interrupt:
Note that Interrupt_Config()
needs to be run in the main code and CNB_Config()
still needs to be defined.
Change Notice (CN) is one of the simplest and commonly used interrupt sources especially when dealing with inputs. As the name suggests, an interrupt is generated once the state of an input pin changes. Sir Kevin talks more about the interrupt in this short video (1:18).
The PIC32 MM family has a CN module that generates interrupts for each port (A, B, and C) and each port has 5 amin SFRs associated with the CN:
CNCONx |
CN Control - Contains the ON and CNSTYLE bits | |
ON |
When this bit is 1, change notice on port x is enabled and disabled if this bit is 0. | |
CNSTYLE |
CN Style Bit There are two available styles which are used depending on the value of this bit: 1 - Edge Style: Change notice interrupt is issued when an edge transition is affected. This style is used with the CNFx bits. 0 - Mismatch Style: Change notice interrupt is issued when the current reading on the input port value is different from the previous reading. This style is used with the CNSTATx bits. |
|
CNEN0x |
CN Enable 0 For the bits in this register that are set to 1, a positive transition (i.e. from logic low to logic high) on the corresponding port bit will generate an interrupt in edge style. Similarly, a mismatch in port readings will generate an interrupt for the set bits in mismatch style. |
|
CNEN1x |
CN Enable 1 For the bits in this register that are set to 1, a negative transition (i.e. from logic high to logic low) on the corresponding port bit will generate an interrupt in edge style. This register is ignored in mismatch style. |
|
CNSTATx |
CN Status Each bit in this register corresponds to the status of each bit in the port that it is associated to. If the a bit is read to be 1, then a change notice event has occurred for the corresponding pin. |
|
CNSFxM |
CN Flag Works similarly to the CNSTATx register but is only enabled when in in edge style. This flag needs to be cleared to enable succeeding CN interrupts. |
To properly configure the CN module, we just need to go over all the SFRs mentioned. Additionally, the CN modules only work with pins configured as inputs so we need to consider that as well.
As an example, let us consider using the push button S1
in the PIC32MM Curiosity DevBoard. The goal is to toggle the status of LED1 each time the push button is pressed:
S1
is connected to RB7
and we make sure that RB7
is configured as an input.CNEN1B
to 1.CNFB7
bit just in case anything unexpected happened.CNFB7
bit must be cleared every time the interrupt is triggered to allow it to trigger the next time.The commands to do these steps are highlighted in comments in the code seen on the following page:
Our main function should call InterruptConfig()
and create an infinite loop to make sure the PIC doesn't enter sleep mode. What's left for us to do is just define Debounce()
and ToggleLED()
and it should work.
But we didn't even call CNB_ISR()
. How would we know it would run? Well that is the advantage of interrupts, we don't have to call the ISR. The microcontroller will do that for you as long as you correctly configured it.
It didn't work? make sure you added the missing function definitions, included all the needed libraries and set up your main function well. You might also want to double-check your code as you may have mistyped anything. One usual suspects are the ISR priority level is different from the level set in the priority bits. If you still can't get it to work, contact your lab handler immediately.
To make sure you're catching up with the CN interrupt, let us emulate an AND gate where, the inputs are the pushbutton states. Here are the specifications, constraints, and additional notes for this activity:
CNSTATx
flags instead of the CNFx
flags in this mode.Let's apply what you have learned about simple GPIO in a microcontroller. For this exercise, you will utilize LED1 and LED2, as well as PB1. Write a program named blinky_pushy.c that would achieve the following specifications.
Last Digit of Student Number | Pattern |
---|---|
0-3 | Both LEDs ON for 1s → LED1 ON, LED2 OFF for 1s → LED1 OFF, LED2 ON for 1s → Both LEDs OFF for 1s → repeat |
4-6 | Both LEDs ON for 1s → LED1 OFF, LED2 ON for 1s → LED1 ON, LED2 OFF for 1s → Both LEDs OFF for 1s → repeat |
7-9 | LED1 ON, LED2 OFF for 1s → Both LEDs ON for 1s → LED1 OFF, LED2 ON for 1s → Both LEDs OFF for 1s → repeat |
eee158_lab3_[STUDENT_NUMBER]
, e.g. eee158_lab3_201912345
.
pushy_with_interrupts.c
, blinky_pushy.c
) are included and well documented.In this module, we discussed the different configurations in interfacing with GPIO devices such as LEDs and pushbuttons. We used the MPLAB simulator as a means to observe the state changes when the different states of an input or output device is changed. We also used polling and interrupts in combining the functionalities of different input and output devices, as seen in the exercises given. This is a good starting point for the next exercises in EEE 158.
For this module, there are a total of 2 activities:
Activity | Points |
---|---|
Exercise 1 | 40 |
Challenge | 60 |
Total | 100 |