Embedded C Programming with the PIC18F14K50 - 8. Non-blocking operations with Buttons and LEDs

Published


Hello again! This time we will put aside the 7-segment indicator and will return to buttons and LEDs but now we will double their number. Today, I’ll tell you how to perform non-blocking operations with LEDs and buttons. To understand what I mean, let’s consider part of the code from tutorial 2:

LATCbits.LATC0 ^= 0x01;//Toggle RC0 pin

__delay_ms(500); //500ms delay

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 4:

if (PORTBbits.RB6 == 0) //If button is pressed (RB6 is low)

{

__delay_ms(20); //Then perform the debounce delay

if (PORTBbits.RB6 == 0) //If after the delay RB6 is still low

{

while (PORTBbits.RB6 == 0); //Then wait while button is pressed

__delay_ms(20); //After button has been released, perform another delay

if (PORTBbits.RB6 == 1) //If the button is released after the delay

{

LATCbits.LATC0 ^= 0x01;//Then perform the required action

}

}

}

You can see there are some short delays which we could actually ignore, but there is a line

while (PORTBbits.RB6 == 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. This is yet 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 at least quite hard.

Now, as we understand the issue, let’s formulate the task for this tutorial:

  1. Implement the parallel and independent processing of two buttons.
  2. 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.
  3. 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).

Schematics diagram with the PIC18F14K50 with LEDs and buttons
Figure 1 - Schematics diagram with the PIC18F14K50 with LEDs and Buttons

This time I decided to connect all parts to port B: pushbuttons S1 and S2 to 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 new to review here.

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.

First, let’s get into a deeper understanding of how interrupts work at a lower level. If this doesn’t interest you, feel free to skip these next two paragraphs.

To run interrupts, the MCU has a so-called interrupt vector. It is a table of addresses in the non-volatile memory of the MCU. Each address in this table corresponds to some interrupt source. When an interrupt happens, the program counter is stored in a stack, and then jumps to the corresponding address of this table. In this address there should be a “jump” instruction (or something similar, like “GOTO” in PIC MCUs) which will direct the program counter to the start of the interrupt subroutine. At the end of this subroutine there is a special command that restores the initial value of the program counter from the stack, and the normal flow of the program continues. If the “jump” command is not found at the specified interrupt vector address, the program can break, and start from the beginning. Therefore, it’s important to create an interrupt subroutine if you enable the corresponding interrupt. I had this situation several times when I forgot to create the subroutine and the program permanently restarted whenever an interrupt happened. I couldn’t understand why and it was very frustrating.

In the PIC18F14K50 MCU there is a strange interrupt organization (in my opinion, anyway, there’s probably a good reason that the developers used this approach). So the interrupt vector of this MCU consists of only one or two addresses (depending on the mode). This means that all interrupt sources activate the same address in the interrupt vector, and thus there is a common subroutine into which all interrupts are processed. In another mode there are two addresses: for high-priority interrupts and for low-priority interrupts. We will consider this mode later when we have more interrupts in one program.

Interrupt Registers

The same as all other modules, interrupts are configured and controlled via special registers. To work with interrupts, the PIC18F14K50 has 10 registers but we will only consider two of them in more detail, and talk about the others when we need them later.

  • RCON - Reset control register
  • INTCON, INTCON2, INTCON3 - Interrupt control registers
  • PIR1, PIR2 - Peripheral interrupt request registers
  • PIE1, PIE2 - Peripheral interrupt enable registers
  • IPR1, IPR2 - Peripheral interrupt priority registers

If you have a good memory, you remember the INTCON2 register. We used its bit RABPU to enable the pull-up resistors in tutorial 4.

RCON register, as follows from its name, allows control of the state of the MCU upon reset. We will consider the majority of its bits later, and now we need only the bit #7 - IPEN (Interrupt Priority ENable bit). Setting this bit to “1” enables the priority levels on interrupts, while resetting it to “0” disables the priority levels. As I mentioned before, we don’t need to prioritize the interrupts in the current program, so we will reset this bit to “0”.

INTCON register interests us more so we will consider it in more detail. It has the following bits:

  • bit #7 - GIE/GIEH (Global Interrupt Enable bit). It has two meanings depending on the value of the IPEN bit.
    • If IPEN = 0: Setting this bit to 1 enables all unmasked interrupts, resetting it to 0 disables all interrupts including peripherals.
    • If IPEN = 1: Setting this bit to 1 enables all high-priority interrupts, resetting it to 0 disables all interrupts including low priority.
  • bit #6 - PEIE/GIEL (Peripheral Interrupt Enable bit). It also has two meanings depending on the value of the IPEN bit.
    • If IPEN = 0: Setting this bit to 1 enables all unmasked peripheral interrupts, resetting it to 0 disables all peripheral interrupts.
    • If IPEN = 1: Settings this bit to 1 enables all low-priority interrupts, resetting it to 0 disables all low priority interrupts.
  • bit #5 - TMR0IE (Timer0 Overflow Interrupt Enable bit). Setting this bit to 1 enables the Timer0 overflow interrupt, resetting this bit to 0 disables the Timer0 overflow interrupt.
  • bit #2 - TMR0IF (Timer0 Overflow Interrupt Flag bit). This bit is set to 1 by hardware. If it’s set to 1 then Timer0 register has overflowed, if it’s 0 then Timer0 register didn’t overflow. When set, this bit should be cleared by software.

The rest of the bits of this register control the pin state change interrupts, and will be considered in the next tutorials as well as other interrupt registers.

I think some explanation is required regarding the interrupts types mentioned in the registers description.

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.

To control Timer0, the T0CON register is used. Let’s consider it in detail.

  • bit #7 - TMR0ON (Timer0 on) . Setting this bit to 1 enables Timer0, resetting it to 0 disables Timer0;
  • bit #6 - T08BIT (Timer0 8-bit). Setting this bit to 1 sets the 8-bit mode, resetting it to 0 sets the 16-bit mode;
  • bit #5 - T0CS (Timer0 Clock Source Select). When this bit is set to 1, the timer is clocked with the pulses applied to the T0CKI pin. When this bit is reset to 0, the timer is clocked with the internal instruction cycle clock (CLKOUT). This clock is 4 times slower than the CPU clocking frequency, so it’s also called Fosc/4.
  • bit #4 - T0SE (Timer0 Source Edge Select). When this bit is set to 1, the timer increments on a high-to-low transition on the T0CKI pin. When it is reset to 0, the timer increments on a low-to-high transition on the T0CKI pin.
  • bit #3 - PSA (Timer0 Prescaler Assignment). When this bit is set to 1, the prescaler is not assigned, and the timer is clocked directly by the input clock. When this bit is reset to 0, the prescaler is assigned, and the input clock is divided by the prescaler.
  • bits #2-0 - T0PS (Timer0 Prescaler Select):
T0PS2T0PS1T0PS0Prescaler Value
0001:2
0011:4
0101:8
0111:16
1001:32
1011:64
1101:128
1111:256

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.

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

#define _XTAL_FREQ 1000000 //CPU clock frequency

#define DEBOUNCE 30 //Button debounce

#define LED1_PERIOD 500 //LED1 period

#define LED2_PERIOD 250 //LED2 period


#include <xc.h> //Include general header file


uint32_t tick; //Milliseconds counter

uint8_t previous1, previous2; //Previous states 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 __interrupt() InterruptManager (void) //Interrupt vector

{

if ((INTCONbits.TMR0IE == 1) && (INTCONbits.TMR0IF == 1)) //If Timer0 interrupt is enabled and Timer0 interrupt flag is set

{

tick ++; //Increment the milliseconds counter

INTCONbits.TMR0IF = 0; //Reset the timer0 interrupt flag

TMR0L = 6; //Set the initial counter value as 6 to set the Timer0 period as exactly 1 ms

}

}


void main(void) //Main function of the program

{

//GPIO configuration

TRISBbits.TRISB4 = 0; //Configure RB4 pin as output

TRISBbits.TRISB5 = 0; //Configure RB5 pin as output

TRISBbits.TRISB6 = 1; //Configure RB6 pin as input

TRISBbits.TRISB7 = 1; //Configure RB7 pin as input

WPUBbits.WPUB6 = 1; //Enable pull-up resistor at RB6 pin

WPUBbits.WPUB7 = 1; //Enable pull-up resistor at RB7 pin

INTCON2bits.nRABPU = 0; //Allow pull-up resistors on ports A and B

//Timer 0 configuration

TMR0L = 6; //Set the initial counter value as 6 to set the Timer0 period as exactly 1 ms

T0CONbits.PSA = 1; //Prescaler is not assigned

T0CONbits.T08BIT = 1; //8-bit mode

T0CONbits.T0CS = 0; //Internal instruction cycle clock

T0CONbits.TMR0ON = 1; //Timer0 is enabled

//Interrupts configuration

INTCONbits.GIE = 1; //Enable global interrupts

INTCONbits.TMR0IE = 1; //Enable Timer0 overflow interrupt

RCONbits.IPEN = 0; //Disable priority level on interrupts

while (1) //Main loop of the program

{

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 ((PORTBbits.RB7 == 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

{

LATBbits.LATB4 = 0; //Then turn off the LED1

}

}

else if ((PORTBbits.RB7 != 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 ((PORTBbits.RB6 == 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 ((PORTBbits.RB6 != 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

LATBbits.LATB5 = 0; //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

LATBbits.LATB4 ^= 0x01; //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

LATBbits.LATB5 ^= 0x01; //And toggle the LED2

}

}

}

}

The program seems to be quite large but get used to this - the next ones will be even larger and more complex.

Please pay attention, that in this program I removed the lines regarding the configuration bits but you still need to add them.

In line 1, we still define the _XTAL_FREQ macro, but actually it’s not needed in this program as we will not use the delay functions here.

In line 2, 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 3 and 4, we define the periods between the LED1 and LED2 toggling respectively. According to the task, LED1 should blink with a 1s period, so the toggling should happen every 500 ms. The same for LED2.

Now let’s consider the variables that we need in our program. They are declared in lines 8-12.

In line 8, 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 9, 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 10, 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 11, 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 12, 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 14-22, there is an interrupt subroutine. Let’s consider it now because there is nothing too hard in it. In line 14 there is a name of the interrupt subroutine. You can give any name to it, here I called it InterruptManager. There are some specific things though. The type of the function should be “void”, and the parameter list also should be “void”. Besides, before the function name you should write the special word __interrupt() which tells the compiler that this function will not be called by the user's code but will be used to process the interrupts.

In line 16 we check to see if the Timer0 overflow interrupt is enabled (INTCONbits.TMR0IE == 1) and if the Timer0 overflow flag was set (INTCONbits.TMR0IF == 1). These two conditions are enough to determine that the interrupt has happened. We need to check both these conditions because the interrupt flag can be set even when the interrupt is masked. If we had several interrupts, we should check these two conditions for each of them inside this function.

Since we have figured out that the Timer0 overflow interrupt has happened, now we can implement the required code. The useful “payload” of this function is just line 18 where we increment the tick variable. In line 19 we reset the interrupt flag. This action is mandatory because without it the flag will remain “1” and we will be stuck inside the interrupt subroutine forever and never return to the main program. So this is another important thing when you deal with interrupts: always check carefully in the datasheet if you need to reset the interrupt flag manually and, if yes, do this. Otherwise you risk having a non-working program.

In line 20, we assign the value 6 to the TMR0L register. We will consider this action later when we talk about the configuration part of the program because we do the same thing in line 35.

Let’s now move to the main function of the program which is located at lines 24-102. As usual, it starts with the initialization part (lines 26-43). Let’s consider it.

In lines 27-33, we configure the GPIO of the MCU: RB4, RB5 as outputs (lines 27-28), as LED1 and LED2 are connected to these pins; RB6, RB7 as inputs (lines 29-30) as buttons S1 and S2 are connected to them. Also we enable the pull-up resistors at pins RB6, RB7 (lines 31-32) and allow the pull-up resistors at all pins of ports A and B (line 33). Actually we could compress this part into three lines by using the operation with bytes:

TRISB = 0xC0;

WPUB = 0xC0;

INTCON2bits.nRABPU = 0;

But I think the initial record is more illustrative even though it spends several bytes more.

In lines 35-39 there is a Timer0 configuration. Let’s start its consideration with line 36, and then return to line 35.

So in line 36 we deassign the prescaller from the timer input, then set the 8-bit mode (line 37), set the internal source Fosc/4 as the timer clock source (line 38) and finally enable the timer (line 39). After all these actions we will have the timer which is clocked with the frequency of 1MHz/4 = 250 kHz. As the timer is 8 bit, its period will be 1/250 ms * 256 = 1.024ms. If we don’t need high precision, we can leave it as is. But we want to strictly stick to the task and have a period of exactly 1ms. To do this, we can start counting not from 0 but from 6, in this case the period will contain only 250 ticks instead of 256, and the timer period will be 1/250ms * 250 = 1ms. To start counting from 6, we should assign this value to the timer register TMR0L every time after the timer overflows. This is exactly what we do in lines 20 and 35.

In lines 41-43, we configure the interrupts. In line 41 we enable global interrupts then in line 42 we unmask (enable) the Timer0 overflow interrupt. And finally in line 43 we disable the priorities on interrupts (actually we could skip this step as the priorities are disabled by default but better to do it explicitly if we are not running out of memory).

And now all the configuration is completed, so we can move to the main loop of the program (lines 44-101).

The main loop consists of four parts: processing button S1 (lines 46-66), processing button S2 (lines 68-82), blinking LED1 (lines 84-91), and blinking LED2 (lines 93-100). 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 46). If it is, we assign the current time to the variable lastPress1 (line 48). After implementing this line, the condition in line 46 will become false for a time, defined by the DEBOUNCE macro. I will explain why we need this a bit later.

In line 49 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 51). At the next loop iteration, which will happen after the DEBOUNCE time (because of the checking in lines 46-48) the previous1 value will be 0, and we will not enter this branch again. So lines 51-60 are implemented only one time when the button is just pressed and we can implement some useful action here. According to the task, upon pressing S1 we need to start blinking the LED1, and at the next button press we need to stop blinking LED1 and turn it off. Here we need to toggle some flag that indicates if LED1 is blinking or not. We have defined such a flag variable and called it blink_enable1, and we toggle it in line 52. We will process this variable later, in lines 84-91, and for now let’s finish with button S1.

In line 53, 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 55) to indicate that the LED1 blinking starts at this exact moment. Otherwise (line 57) we turn off LED1 (line 59).

In line 62, 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 64 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 68-82 there is a similar algorithm for button S2. The main checkings and conditions are the same as for button S1, so I will just review 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 72-75 is implemented when we press the button, and here we set the variable blink_enable2 to 1 (line 74) to start LED2 blinking. The branch in lines 77-81 is implemented when the button is being released, and here we set the blink_enable2 as 0 (line 79) and turn off the LED2 (line 80).

That’s all about the button processing algorithm. Its main features are:

  1. 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.
  2. 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.
  3. 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 84, we check the value of the blink_enable1 variable which, as I mentioned before, indicates if the LED1 should blink. In line 86 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 88) and toggle the LED1 state (line 89). By means of the lines 86 and 88 we toggle the LED1 once in 500 ms, and don’t use any delay functions.

The lines 93-100 are equal to the lines 84-91 but are related to the 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.

As homework, I suggest you increase the Timer0 period to 10 ms instead of 1 ms. This will require using the prescaler and you will need to change the initial value of the TMR0L value. Don’t forget to decrease the used macros 10 times to make the same delays.

The next time we’ll implement the same task but using the MCC. You will see how much easier the visual configuration of the timer and interrupts is.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?