Efficient Button Integration With Non-blocking Operations Using MCC | Embedded C Programming - Part 9
Published
Hello again! In this tutorial we continue talking about the non-blocking operations with LEDs and buttons. Previously I showed you how to make them without the MCC, and this time we will use it. If you didn’t read the previous tutorial, don’t worry, I’ll explain the theory here as well but not that deeply, without diving into the registers explanation.
Let’s first consider the part of the code from the tutorial 3 to see what I mean when talk about the non-blocking operations:
LED1_Toggle();
__delay_ms(500);
As you see, to perform the delay, we use the __delay_ms function which is based on calling the NOP operation a lot of times. During this function, all other operations are stopped until it’s finished. Such functions are called “blocking” because they block the entire program until their implementation is finished. And if you want to make several LEDs blink with different frequencies, it would not be a trivial task.
Also let’s see how the button processing is implemented in tutorial 5:
if (BUTTON_GetValue() == 0) //If button is pressed
{
__delay_ms(20); //Then perform the debounce delay
if (BUTTON_GetValue() == 0) //If after the delay button is still low
{
while (BUTTON_GetValue() == 0); //Then wait while button is pressed
__delay_ms(20); //After button has been released, perform another delay
if (BUTTON_GetValue() == 1) //If the button is released after the delay
{
LED_Toggle();//Then perform the required action (toggle the LED)
}
}
}
You can see there are some short delays which we could actually ignore, but there is a line
while (BUTTON_GetValue() == 0);
in which we wait the entire time the button is pressed. So if you hold the button, all the MCU operations are stopped until you release the button. Thus, this is another blocking operation.
So, you see, that the regular algorithms of the buttons and LED processing consist of blocking operations and functions, which make simultaneous operations with several buttons or LEDs impossible or quite hard.
Now, as we understand the issue, let’s formulate the task for this tutorial:
- Implement the parallel and independent processing of two buttons.
- Upon pressing the first button, start to blink LED1 at the frequency of 1 Hz. At the next pressing of the button, stop blinking LED1 and turn it off.
- Upon pressing and holding the second button, blink LED2 with the frequency of 2 Hz. Stop the blinking and turn off LED2 when the second button is released.
So, as you can see, here we need to implement the independent operation of two buttons and two LEDs.
Schematics Diagram
Let’s consider the schematic diagram that implements the given task (Figure. 1).
This time I decided to connect all parts to port B: pushbuttons S1 and S2 to the pins PB7 and PB6, and LED1 and LED2 to RB4 and RB5, respectively. The connection of the LEDs and buttons has been described in the previous tutorials, so there is nothing to stop on.
Before describing the program code we first need to consider two new MCU modules which will be used in it: interrupt controller and Timer0. So the theoretical part will be quite meaty this time.
Explanation of Interrupts
We will start with interrupts. Even though the PIC10F200 didn’t have it, it’s a fundamental concept in the microcontrollers’ world. In simple words, interrupt is the special mechanism that allows one to break the normal flow of the program, and immediately invoke a special function, called an “interrupt subroutine”. After completion of implementation of this subroutine, the program counter returns to the instruction that had to be implemented before the interrupt happened, and the normal program flow restores.
The main difference between the interrupt subroutine and the usual subroutine is that the first one is not called explicitly from the program code but is invoked automatically at any moment.
There is a list of events that can cause an interrupt. It’s specific to every MCU, but there are some common ones which are present almost everywhere:
- changing of the state of a specific pin;
- timer overflow;
- ADC conversion complete (if an ADC module is present);
- communication module has received data or is ready for transmission (SPI, I2C, USART, if present);
- capture/compare event of the timer module has occurred.
In the current tutorial we will use the timer overflow interrupt. But we’ll talk about it momentarily.
All interrupts in PIC18F14K50 are maskable which means that they have a dedicated bit that masks or unmasks them allowing them to execute regardless of the other interrupts. For example, Timer0 overflow interrupt is masked by the TMR0IE bit. Masking or unmasking basically means that you can choose whether that specific item generates an interrupt under the conditions which normally would generate them. So, if the Timer0 overflow interrupt is masked, when Timer0 overflows, it will not generate an interrupt even if the GIE bit is set.
Also, there are so-called peripheral interrupts which are caused by the different peripheral modules like timers, communication modules, ADC, etc. There are several interrupts though that are not related to the peripherals: all types of GPIO state change interrupts (RAB, INT0, INT1, INT2), and for some reason Timer0 overflow interrupt. So to enable the Timer0 interrupt we only need to set the TMR0IE and GIE bits high, and no need to set the PEIE bit.
Now we have all the required information to understand the interrupt part of the program. If you want to get more information about interrupts, you can refer to chapter 7 of the PIC18F14K50 datasheet. And now let’s now consider the Timer0 module.
Timer0 Module
If you read the PIC10F200 tutorials, you may remember something about the Timer0 module. But in the PIC10F200 MCU it was very limited. It couldn’t even generate interrupts, which made it almost useless in the majority of applications in which timers are usually used. Here, Timer0 is a fully functioning timer with all required features.
Actually, PIC18F14K50 has four timers: Timer0, Timer1, Timer2, and Timer3. And all of them are different. Really this is also weird for me. Usually simple 8-bit MCUs have two or rarely three timer types, but here we have four! We will consider Timer1 to Timer3 in the next tutorials but now the object of our interest is Timer0.
Timer0 is very simple yet quite functional. It has these features:
- Software selectable 8-bit or 16-bit modes;
- 8-bit software programmable prescaler;
- Readable and writable timer registers;
- Selectable clock source (internal or external);
- Edge select for external clock;
- Interrupt on timer overflow.
Timer0 can act as a timer (when it’s clocked with the internal source) or as a counter (when it counts the pulses that come to one of the MCU pins). In both cases it can be configured in 8-bit mode (in this case it counts from 0 to 255) or in 16-bit mode (counts from 0 to 65535). Also, in both modes the 8-bit prescaler can be applied. It divides the input timer frequency by a value of 2 up to 256.
Timer0 has two registers TMR0H and TMR0L which consist of the high and low byte of the current timer counter respectively. These registers can be read or written at any moment by the software. In 8-bit mode, only TMR0L is used.
On Timer0 overflow the interrupt can be generated, which we talked about in the previous section. It happens when the timer register changes its value from 255 to 0 in 8-bot mode, or from 65535 to 0 in 16-bit mode.
When Timer0 works as a counter for external pulses, it can increment either on the rising or falling edge of the pulse.
And this is all that we need to know about Timer0. If you still want to know more, please refer to the datasheet of the PIC18F14K50 MCU, chapter 10.
MCC Configuration
The same as in the previous tutorials, create a new project, run the MCC, configure the System Module, change the package type, then configure the Pin Module according to the schematics diagram (Figure 1), you will have the following page (Figure 2):
There is nothing new here so far, so I will skip the explanation of this part.
What we need to do is to add the Timer0 module to our project. To do this, we go to the Device Resources tab, and open the Timer drop down list, then click on the green plus at the Timer0 timer (Figure 3).
When you select it, it will appear in the Project Resources, and also the TMR0 tab will be opened. You need to change some parameters in it according to Figure 4.
We don’t need a prescaler in our current project so we don’t enable it, also we leave the timer mode as 8-bit. We need to change the clock source from external to Fosc/4. Also, we need to enable the timer interrupt. Finally, we set the timer period as 1ms and callback function rate as 1. The last parameter sets the number of interrupts that occur before it invokes the callback function. We want to invoke it every time, so we set this parameter as 1.
Those are all the parameters required to be set for Timer0. Now let’s switch to the Interrupt Module and make sure everything is configured correctly there (Figure 5).
Here we should uncheck the point “Enable High/Low Interrupt Vector Priority” because we have only one interrupt, so no need to prioritize it. The state of the checkbox “Single ISR per Interrupt” can be any because there is only one interrupt vector at this MCU. And finally make sure that the TMR0 interrupt is enabled (the checkbox is set).
That’s all the settings we need for the current project. Now we can press the Generate button and open the “main.c” file. But before we proceed to the programming code, let’s first consider the algorithm.
Program Algorithm
Now let’s talk about the algorithm of the non-blocking operations, and why we need all these timers and interrupts. Actually, the only thing that is required from the timer is to generate interrupts every 1 ms. If you are familiar with the Arduino platform, you should know that it has a function millis() that returns the number of milliseconds passed from the MCU start. Inside the Arduino core this function is implemented in the same way - by using the 1 ms timer.
OK, we now have the source of milliseconds but what do we do with them now? If we need to perform a non-blocking delay, we assign the current milliseconds value to some variable, and then compare this variable with the increasing milliseconds counter. Once the difference is equal to the desired delay, we perform the required action.
To better understand what I mean, let’s consider the program code that implements the given task.
Program Code Description
#include "mcc_generated_files/mcc.h"
#define DEBOUNCE 30 //Button debounce
#define LED1_PERIOD 500 //LED1 period
#define LED2_PERIOD 250 //LED2 period
uint32_t tick; //Milliseconds counter
uint8_t previous1, previous2; //Previous state of the buttons
uint32_t lastPress1, lastPress2; //Time of the buttons last press
uint8_t blink_enable1, blink_enable2; //flag to enable the LEDs blinking
uint32_t led1_start, led2_start; //Time of the LEDs blinking start
void timer0_overflow (void)
{
tick ++;
}
void main(void)
{
// Initialize the device
SYSTEM_Initialize();
TMR0_SetInterruptHandler(timer0_overflow);
// Enable the Global Interrupts
INTERRUPT_GlobalInterruptEnable();
while (1)
{
if (((tick - lastPress1) > DEBOUNCE)) //If the time between the last press and the current time is bigger than the DEBOUNCE
{
lastPress1 = tick; //Set the last press time as the current time
if ((SW1_GetValue() == 0) && (previous1 != 0)) //If the current button state is low and previous state is high
{
previous1 = 0; //We reset the previous state to 0 to prevent implementation of this branch the next time
blink_enable1 ^= 1; //Toggle the "blink_enable1" flag to enable or disable LED1 blinking
if (blink_enable1) //If LED1 blinking is enabled
{
led1_start = tick; //Then set the time of the LED1 blinking start as the current time
}
else //If LED1 blinking is disabled
{
LED1_SetLow(); //Then turn off the LED1
}
}
else if ((SW1_GetValue() != 0) && (previous1 == 0)) //If the current button state is high and previous state is low
{
previous1 = 1; //Then we set the previous state as 1 to prevent implementation of this branch the next time
}
}
if (((tick - lastPress2) > DEBOUNCE)) //If the time between the last press and the current time is bigger than the DEBOUNCE
{
lastPress2 = tick; //Set the last press time as the current time
if ((SW2_GetValue() == 0) && (previous2 != 0)) //If the current button state is low and previous state is high
{
previous2 = 0; //We reset the previous state to 0 to prevent implementation of this branch the next time
blink_enable2 = 1; //Set the "blink_enable2" flag to enable LED2 blinking
}
else if ((SW2_GetValue() != 0) && (previous2 == 0)) //If the current button state is high and previous state is low
{
previous2 = 1; //Then we set the previous state as 1 to prevent implementation of this branch the next time
blink_enable2 = 0; //Reset the "blink_enable2" flag to disable LED2 blinking
LED2_SetLow(); //Turn off the LED2
}
}
if (blink_enable1) //If "blink_enable1" flag is set
{
if ((tick - led1_start) > LED1_PERIOD) //If the time between the LED1 blinking start and the current time is bigger than the LED1_PERIOD
{
led1_start = tick; //Then set the time of the LED1 blinking start as the current time
LED1_Toggle(); //And toggle the LED1
}
}
if (blink_enable2) //If "blink_enable2" flag is set
{
if ((tick - led2_start) > LED2_PERIOD) //If the time between the LED2 blinking start and the current time is bigger than the LED2_PERIOD
{
led2_start = tick; //Then set the time of the LED2 blinking start as the current time
LED2_Toggle(); //And toggle the LED2
}
}
}
}
This program is a bit shorter than the one without the MCC but this is only because the initialization part is hidden inside the MCC generated files. Let’s consider this program in more detail.
In line 3, we define the macro DEBOUNCE which represents the debounce time in ms when buttons are pressed and released. I defined it as 30 ms because my buttons are not that good, and 20 ms is not enough. You can select the value of 10-50 ms depending on your buttons’ quality.
In lines 4 and 5, we define the periods of LED1 and LED2 toggling respectively. As according to the task the LED1 should blink with the 1s period, the toggling should happen every 500 ms. The same with LED2.
Now let’s consider the variables that we need in our program. They are declared in lines 8-12.
In line 7, we declare the variable tick of uint32_t type. This type has an unsigned integer value of 32 bits, which has the range of 0 - 4 294 967 296, so the milliseconds counter tick can count up to 4 294 967 296 ms or about 50 days. Then it will reset to 0 and start counting again.
In line 8, we declare two variables: previous1 and previous2 that represent the previous state of the pins to which the buttons S1 and S2 are connected, respectively.
In line 9, we declare two variables: lastPress1 and lastPress2 that represent the tick value at the moment when the button S1 or S2 was pressed.
In line 10, there are two more variables: blink_enable1 and blink_enable2 which are the flags that indicate whether the blinking for LED1 or LED2 is enabled or not.
In line 11, the last couple of variables are declared: led1_start and led2_start that represent the tick value at the moment when LED1 or LED2 started blinking.
In lines 13-16, there is a Timer0 interrupt callback function. The name of the function can be whatever you want, in the current program I selected the name “timer0_overflow”. But the type of the function should be void, and the parameters list should also be void.
Inside this callback function we just increment the tick variable (line 15). Please pay attention, that this function is called automatically when the timer interrupt happens but not from the user code.
Let’s now move to the main function of the program which is located in lines 18-85. It starts with the function SYSTEM_Initialize (line 21) generated by the MCC, which initializes all used peripheral modules of the MCU.
In line 23, there is a function TMR0_SetInterruptHandler(timer0_overflow). As follows from its name, it sets the function that will act as the interrupt handler. In our case, it’s the “timer0_overflow” function which was described earlier. I’ve found the function TMR0_SetInterruptHandler in the file “tmr0.h”:
/**
@Summary
Set Timer Interrupt Handler
@Description
This sets the function to be called during the ISR
@Preconditions
Initialize the TMR0 module with interrupt before calling this.
@Param
Address of function to be set
@Returns
None
*/
void TMR0_SetInterruptHandler(void (* InterruptHandler)(void));
This approach seems to be very convenient as you can define any function and then appoint it as an interrupt handler. Also, if you forget to do this, the default callback function will be called, so your code won’t be broken.
In line 25, there is a function INTERRUPT_GlobalInterruptEnable(). If you read the comments generated by the MCC carefully, you might notice that it was generated automatically along with the other interrupt-related functions, but commented. So the only thing we have to do here is to uncomment this function and optionally delete the rest of the comments.
And now all the configuration is completed, so we can move to the main loop of the program (lines 27-84).
The main loop consists of four parts: processing button S1 (lines 29-49), processing button S2 (lines 51-65), blinking LED1 (lines 67-74), and blinking LED2 (lines 76-83). Let’s consider them in more detail.
First, we check if the time between the current moment and the last button press is bigger than a debounce (line 29). If it is, we assign the current time to the variable lastPress1 (line 31). After implementing this line the condition in line 29 will become false for a time, defined by the DEBOUNCE macro. I will explain why we need this a bit later.
In line 32, there is a very important check. We check if the current button state is 0 and the previous button state is 1. If this condition is true, this means that we caught the moment when we just pressed the button. Now we need to reset the previous button state to 0 (line 34). At the next loop iteration, which will happen after the DEBOUNCE time (because of the checking in lines 29-31) the previous1 value will be 0, and we will not enter this branch again. So lines 34-43 are implemented only one time when the button is pressed and we can implement some useful action here. According to the task, upon pressing button S1, we need to start blinking LED1, and at the next button press we need to stop blinking LED1 and turn it off. So here we need to toggle some flag that indicates if the LED1 is blinking or not. We have defined such a flag variable and called it blink_enable1, and we toggle it in line 35. We will process this variable later, in lines 67-74, and for now let’s finish with the button S1.
In line 36, we check the state of the blink_enable1 variable. If it’s 1 (which means that the LED1 blinking is enabled) then we assign the current tick value to the led1_start variable (line 38) to indicate that the LED1 blinking starts at this exact moment. Otherwise (line 40) we turn off LED1 (line 42).
In line 45, there is another important check where we check if the current button state is 1 and previous button state is 0. This means the moment when we just released the button. In line 47 we set the previous1 value to 1 to prevent entering this branch in the next loop iteration. So here we can do some action which we want to implement one time when the button is being released. As there is no such action for button S1, we do nothing here.
In lines 51-65 there is a similar algorithm for button S2. The main checkings and conditions are the same as for button S1, so I will just go over the differences. According to the task, we need to start blinking LED2 when the button is pressed and stop blinking when the button is released. Let’s see how this is implemented in code.
Branch in lines 55-58 is implemented when we press the button, and here we set the variable blink_enable2 to 1 (line 57) to start LED2 blinking. The branch in lines 60-64 is implemented when the button is being released, and here we set the blink_enable2 as 0 (line 62) and turn off the LED2 (line 63).
That’s all about the button processing algorithm. Its main features are:
- We check the button state not at every main loop iteration but in a time defined by the DEBOUNCE value. This allows us to implement the debounce delay for the buttons without using the actual delay function.
- We have two branches, one of which allows you to implement the action when the button is being pressed, and another allows you to implement something when the button is being released.
- You can add a third branch when the current and previous button states are both low. In this case you can implement some actions all the time when the button is held pressed.
Now let’s see how the non-blocking LED blinking is implemented.
In line 67, we check the value of the blink_enable1 variable which, as I mentioned before, indicates if LED1 should blink. In line 69 we check if the difference between the tick value and the led1_start value is higher than the LED1_PERIOD (which is 500 ms). When this condition is true, we assign the new tick value to the led1_start variable (line 71) and toggle the LED1 state (line 72). By means of lines 69 and 71 we toggle the LED1 every 500 ms, and don’t use any delay functions.
Lines 76-83 are equal to lines 67-74 but are related to LED2, so I will skip their explanation.
And voila! That’s all about the non-blocking operations. As you can see, the algorithms are not very difficult. Well, they need to use some additional modules but eventually such an approach makes the operation with the buttons much more convenient.
Now you can assemble the circuit according to fig. 1, compile and download the code into the MCU and test how the application works. You may notice that now all buttons and LEDs work independently. You can hold one button, and press another one at the same time - they will still work correctly without any lag. Also, the LEDs will start blinking at the time when you press the corresponding buttons, and independently from one another.
If we compare this code size with the one without using the MCC, we can see that it is about 250 bytes bigger for the MCC version. And, as I said several times, it’s the price we have to pay for the simplicity of programming.
As homework, I suggest you increase the Timer0 period to 10 ms instead of 1 ms. Play with the parameters in the MCC to obtain the required result. Also don’t forget to decrease the used macros 10 times to make the same delays.
Get the latest tools and tutorials, fresh from the toaster.