UBMP4.1 Introductory Programming Activity 1

This activity was designed for UBMP4.1 and is now superseded by a new version for UBMP4.2.

Input-Output Program

One of the first steps in programming microcontroller-based circuits such as UBMP4 is learning to read inputs and control outputs. In this programming activity you will learn how to:

  • read pushbutton input

  • turn on individual LEDs (known as bit, or single-bit output)

  • turn on multiple LEDs at the same time (byte output)

  • work with built-in time delay functions, and

  • combine logical ‘if’ conditions in input statements

If you assembled UBMP4 yourself, you will be able to use the concepts in this programming activity to test the functionality of your microcontroller, pushbuttons, LEDs, and piezo beeper.

This activity will lead you through all of the steps needed to create your first program, including making a programming project, compiling the program files into object code, and downloading and testing the finished object code in your UBMP4.

After that, you can continue on to read a detailed explanation of how the program works, as well as work through some program analysis activities and programming challenges designed to give you a better understanding of the program’s operation. Let’s get started!

Step 1 - Create the program project in MPLAB

To create this first program, you will need to make a program project in an IDE (Integrated Development Environment). An IDE manages all of the files and settings for a programming project, and some IDEs can download programs into their target microcontroller devices.

PICmicro programs can be created in two ways: using Microchip Technology’s MPLAB X IDE running natively on a computer, or using MPLAB Xpress IDE running in a web browser. Either one is fine for this programming activity, and you can learn more about each IDE and how to start a project by following the steps in their getting started pages.

Get started with the MPLAB X desktop IDE.

Get started with MPLAB Xpress cloud IDE.

Do you already know how to use MPLAB X, or MPLAB Xpress?

If you are already familiar with either MPLAB X or MPLAB Xpress, you can create your own MPLAB project for this activity. Start by creating a new, stand-alone project, and set it to use the PIC16F1459 as the target microcontroller. Then, either download the source code files Intro-1-Input-Output activity (.zip format) to manage the files on your own computer, or clone this project into MPLAB X or MPLAB Xpress from its GitHub repository.

Step 2 - Download and test your project

After creating the project in MPLAB X and successfully compiling, or building, the program, the next step is to download the compiled .hex object code file into the PIC16F1459 microcontroller on your UBMP4.

UBMP4 was designed to use a pre-installed USB bootloader program that enables you to drag and drop the compiled .hex object file from your computer right onto the UBMP4 device when it is connected to your computer’s USB port.

If you assembled a bare-board UBMP4 without a pre-programmed microcontroller, you will need a PICkit-compatible or a similar stand-alone programmer to pre-load the bootloader into the PIC16F1459. Or, you can modify the project settings and program the PIC16F1459 directly into UBMP4 using your programmer, without using the bootloader.

Using the bootloader makes the process of programming the microcontroller easier, and the rest of these instructions will assume you are using the built-in bootloader in your UBMP4. There are three ways to program your UBMP4 using the bootloader. Each is described below.

Programming UBMP4 the first time

Plugging UBMP4 into a USB port with only the bootloader loaded into its memory will mount it on your computer as a storage device named PIC16F1459.

Caution: Always disconnect UBMP4 from any external battery or power supply before connecting to USB.

After UBMP4 is mounted, find the MPLAB X project folder on your computer and copy the .hex file from the MPLAB X project (typically found in the project’s /dist/free/production, or /dist/default/production folder) onto the PIC16F1459 drive. Or, if using MPLAB Xpress, download the .hex file and then copy the downloaded file onto the PIC16F1459 drive.

The program will quickly transfer into UBMP4 over USB, and the PIC16F1459 drive representing UBMP4 will be ejected from your computer. At this point the UBMP4 status LED, LED D1, may quickly blink off, but then will remain lit to indicate that your program is running. Try pressing pushbutton SW2 to see what this program does!

Reprogramming UBMP4

Plugging a previously programmed UBMP4 into your computer’s USB port will run the program currently in its memory. Pressing and releasing pushbutton SW1 causes LED D1 to blink off and then back on, and starts the bootloader. UBMP4 will then be mounted on your computer as drive PIC16F1459, and you will be able to reprogram it by dragging and dropping a new .hex file to the PIC16F1459 drive. The newly downloaded program will begin to run immediately after the file is transferred.

Note that starting the bootloader by pressing and releasing SW1 will only work if your program contains the code to reset the microcontroller and launch the bootloader in its main program loop. If this reset code is accidentally removed from your program, or if your program gets stuck in a loop and is not able to run the reset code, the bootloader will not respond to SW1 presses, but can still be activated manually by temporarily holding SW1 while power is applied.

Activating the bootloader manually

Pressing and holding pushbutton SW1 while plugging in the USB connector will force the bootloader to start manually. If SW1 was pressed unintentionally while plugging UBMP4 in, pressing SW1 again will exit the bootloader and start the program that was previously downloaded into UBMP4.

 

New Concepts

You might be new to C, or new to programming in general – don’t worry, we’ll take it step-by-step. Here are some important terms and things you should know to get started:

  • C programs are often composed of more than one source code file. Separating programs into multiple files makes the code more portable and modular (for example, to easily support a different processor, or a new circuit board design).

  • most C programs include files containing source code instructions (.c file extension), and files containing directives that might normally reside at the top, or head, of a program, called header files (.h file extension).

  • each C program has to include one file containing a main( ) function, which is the start of the program and contains the main body of the program inside its braces { }. In this program, the main( ) function appears as:

    int main(void)
    {
         ...
    }
  • a C statement or expression is a single line command terminated by a semicolon ( ; ) – think of it as being similar to an English language sentence that is terminated by a period.

  • blocks of C statements making up structures or functions are grouped inside curly braces – { }

  • code inside curly braces is indented for readability, but the type and amount of spacing used for indenting doesn’t matter to the compiler – consistent spacing just makes the code easier to read and debug.

  • single-line code comments are started with double slashes – //

  • multi-line code comments start with /* and end with */

  • C source code files are compiled into machine code object files by the C compiler.

  • object files must be programmed or downloaded into the memory of the microcontroller to run.

Program Operation

After successfully creating the program in MPLAB and downloading it into your UBMP4, press SW2 to watch the LEDs light up in a pattern! Read on to develop a better understanding of both how the microcontroller works, and how the program controls it.

Unlike writing programs for typical desktop or laptop computers, one of the most important aspects of microcontroller programming is learning to interact with I/O (input/output) ports and built-in microcontroller hardware devices. These I/O circuits often also connect to interface circuitry on the circuit board, outside of the microcontroller. Let’s start by learning a bit about the I/O circuits and devices built into the UBMP4 board itself, as well as the I/O pins and hardware features of the PIC16F1459 microcontroller at the heart of UBMP4.

UBMP4 I/O information

Each I/O (input/output) device on the UBMP4 circuit board is assigned a part reference name, such as SW2 or D3. The components of these I/O devices are connected to the port pins of the on-board PIC16F1459 microcontroller, and these port pins have their own, unique pin names. For example, as shown on the UBMP4 schematic, SW2 is connected to microcontroller pin RB4.

When writing a program, it is important to know how to refer to the I/O devices – should they be referred to by their part reference names, or microcontroller pin names? Often, the answer depends on whether you are using a single I/O pin, or an entire I/O port, as well as which programming language or software the program is created in.

In this program, every I/O device is called by its part reference name as labelled on the UBMP4 circuit itself. All of these reference names are defined in the UBMP4.h header file that is part of this program’s MPLAB project, and this file was created specifically to help the compiler understand which microcontroller pin each part connects to. Defining the parts in the program this way is very convenient, since it allows individual devices to be controlled using only their reference names. There is no need for us to look up which I/O pin the component connects to. We can simply refer to each part by its part reference hame for all of these example activities.

Going beyond single pins, addressing multiple I/O devices in a port at the same time, or changing the characteristics of the port pins connected to the devices does require a greater understanding of the microcontroller pin names, as well as a bit of familiarity with the internal I/O port structure of the microcontroller. Enter I/O ports…

PIC16F1459 I/O information

PIC microcontroller I/O pins are organized into groups of pins referred to as ports. The PIC16F1459 microcontroller has three 8-bit ports named PORTA, PORTB and PORTC. Not all eight bits of each port are used, since that would require 24 pins, and the PIC16F1459 only has 20 pins, including the pins reserved for power, ground, USB communication, and the USB voltage regulator pin. Accounting for the reserved pins, a total of 15 I/O pins are available for connection to external circuits and devices, split across the three I/O ports.

Within each port, pins are numbered by their bit position. The least-significant bit, the right-most digit in binary, represents bit, or pin, 0 of the port. Checking the UBMP4 schematic, you will see that LED D3 connects to a microcontroller pin labelled RC4, representing PORTC, bit 4. The R in RC4 stands for register (Microchip refers to internal RAM locations as registers), so RC4 is the software register name that is used to identify the external I/O pin, PORTC, bit 4. Or, at least, that is the way it was in older PIC microcontrollers. Later generations use more than one register for each I/O pin, as you will learn, below.

While I/O pin naming seems confusing, remember that you won’t need to look up the pin names on the schematic for these programs since the UBMP4.h MPLAB project file has already made the association between the pin names and part reference names for us. If you open the UBMP4.h header file in MPLAB or a text editor, you will see that a significant part of it is made up of #define statements, including these two lines below:

...
#define D3          LATCbits.LATC4  // LED D3 output
#define LED3        LATCbits.LATC4  // LED D3 output
...

Each #define statement creates a named definition for an input or output device, and assigns it to a PIC16F1459 port pin. Notice that the definitions for both D3 and LED3 both refer to the I/O pin LATCbits.LATC4. It is defined twice for convenience – in this case the name D3 to represents LED D3 on the newer UBMP4 boards, while the same LED’s part reference was LED3 on the UBMP3 and prior boards. Each pin is allowed to have multiple definitions, and any definition used by a program author will be substituted for the proper register and bit number by the C compiler when the program is compiled. The UBMP4.h file can be edited to add additional definitions, as needed.

By now, you will have noticed that D3 is defined by LATC4, and not by RC4, the pin name shown on the schematic. To understand why this change was made from the earlier PIC microcontrollers, we will need to learn a bit more about registers, latches, and tristates…

I/O ports, registers, latches, and tristates

While the external microcontroller pins use short names such as RC4, the software changed to use the long name of the register, PORTC, instead. Or, at least, it did in some microcontrollers. In Microchip parlance, a register is an internal RAM location in the microcontroller. So, register PORTC is the internal RAM register that holds the values (0 or 1) of the PORTC I/O pins. In a newer microcontroller, such as the PIC16F1459 as used in UBMP4, PORTC is one of a pair of RAM registers responsible for input and output through PORTC, called PORTC and LATC (short for Latch C). Two additional registers, TRISC, and ANSELC, control other aspects of the operation of PORTC. Here is a description of what each of the registers associated with PORTC is responsible for:

  • PORTC is the input register associated with the RC(0-7) pins. Reading PORTC will read the state of all input devices connected to the 8 pins of the port. Reading a single pin, like pin RC3, is done by specifying the bit after the port name, eg. PORTC3 in your program code.

  • LATC is the output latch register for the RC(0-7) pins. Writing to the LATC register stores 8 bits of output data in individual latches connected to the output pins. Writing to an output device, such as LATC4, writes one bit of data into the latch associated with just that bit, which in this case would be the RC4 pin, or D3 on UBMP4.

  • TRISC is the tristate control register for the port, and TRISC controls which port C pins will be used as input or output pins. Writing a 0 into a TRISC bit causes the corresponding pin to be an output, and writing 1 into a TRISC bit makes the associated pin an input – think of it as a 0 being like an ‘O’ in output, and 1 being like ‘I’ in input, to help you remember what each TRISC bit represents.

  • ANSELC is the analog select register and determines which of the PORTC pins will be enabled for analog input instead of digital input. Writing a 1 into an ANSELC bit causes the corresponding pin to become an analog input, and writing a 0 into an ANSELC bit makes the pin a digital input instead.

Both of the microcontroller’s other ports, PORTA and PORTB, also have associated LATA, LATB, TRISA, TRISB, ANSELA, and ANSELB registers. PORTA and PORTB also have two extra registers to control internal weak pull-up resistors (which are not built into the hardware of PORTC), called WPUA and WPUB.

It is important to know that on a device power-up or reset, all of the PIC16F1459 microcontroller’s I/O ports are set up as inputs, with all analog inputs enabled, as well as all weak pull-up resistors enabled. This is done for safety, in that input circuits have a high electrical impedance and will therefore not inadvertently activate output devices before the software has had a chance to configure the I/O pins to their proper levels after a power-up or reset. Before writing to an I/O port, our software will need to configure the port using the ANSELx, TRISx, and WPUx registers. We will see exactly how the port configuration is accomplished in a future program. In this program, we will use a pre-made function to configure the ports, so that we can focus on the main parts of the program and learn how to use the I/O devices.

The main program

The Intro-1-Input-Output program (.zip) project is split into four files – this will be the case for most of the projects in these activities as it provides more flexibility for future enhancements than writing the program as a long, single file. The function of each of the four files is described below:

  • Intro-1-Input-Output.c – the main C language program file, containing the main( ) function, and the first file we will examine in more detail, below.

  • UBMP410.c – a C language file containing functions to initialize the microcontroller and to enable it to use the built-in I/O devices on UBMP4.

  • UBMP410.h – a header file that defines the hardware features of UBMP4 for use by both the functions in UBMP410.c as well as for our our programs.

  • PIC16F1459-config.c – a processor configuration file that selects or enables and disables specific hardware features of the PIC16F1459 microcontroller.

The Intro-1-Input-Output.c file is considered to be the main program file because it contains the main function. The main function has to exist as part of every C language program. The contents of the file are shown below, and we will explore each part of the program to understand its purpose and function.

/*==============================================================================
 Project: Intro-1-Input-Output
 Date:    March 1, 2022
 
 This example UBMP4.1 input and output program demonstrates pushbutton input,
 LED (bit) output, port latch (byte) output, time delay functions, and simple
 'if' condition structures.
 
 Additional program analysis and programming activities demonstrate byte output,
 logical condition operators AND and OR, using delay functions to create sound,
 and simulated start-stop button functionality.
==============================================================================*/

#include    "xc.h"              // Microchip XC8 compiler include file
#include    "stdint.h"          // Include integer definitions
#include    "stdbool.h"         // Include Boolean (true/false) definitions

#include    "UBMP4-1.h"         // Include UBMP4.1 constant and function definitions

// TODO Set linker ROM ranges to 'default,-0-7FF' under "Memory model" pull-down.
// TODO Set linker code offset to '800' under "Additional options" pull-down.

// The main function is required, and the program begins executing from here.

int main(void)
{
    // Configure oscillator and I/O ports. These functions run once at start-up.
    OSC_config();               // Configure internal oscillator for 48 MHz
    UBMP4_config();             // Configure on-board UBMP4 I/O devices
	
    // Code in this while loop runs repeatedly.
    while(1)
	{
        // If SW2 is pressed, make a flashy light pattern
        if(SW2 == 0)
        {
            LED3 = 1;
            __delay_ms(100);
            LED4 = 1;
            __delay_ms(100);
            LED5 = 1;
            __delay_ms(100);
            LED6 = 1;
            __delay_ms(100);
            LED3 = 0;
            __delay_ms(100);
            LED4 = 0;
            __delay_ms(100);
            LED5 = 0;
            __delay_ms(100);
            LED6 = 0;
            __delay_ms(100);
        }
        
        // Add code for your Program Analysis and Programming Activities here:

        // Activate bootloader if SW1 is pressed.
        if(SW1 == 0)
        {
            RESET();
        }
    }
}

The program header

The top part of any program, including code comments, include statements, and often function, constant, and variable declarations, is often referred to as the header. The very top of the header contains a multi-line comment block. Comments are used to document code, and their contents are ignored by the C compiler. Comment blocks in the C language are enclosed within /* and */ characters. Single line comments in C follow double slash // characters.

In C language programs, statements that are meant to be part of the header can be located either at the top of the program, in separate files known as header files, or both. Other than the code comments and four #include statements, much of the header information in this program is actually located in separate header files.

#include    "xc.h"              // Microchip XC8 compiler include file
#include    "stdint.h"          // Include integer definitions
#include    "stdbool.h"         // Include Boolean (true/false) definitions

#include    "UBMP410.h"         // Include UBMP4.1 constant and function definitions

Below the top comment block in the header are the four #include directives shown above. These statements include the contents of the named header files into our program. Although the contents of the header files become a part of our program, the actual code within these first three header files is hidden from our view and will not actually be visible anywhere in our MPLAB project (though there are ways to find it).

The last include statement, #include UBMP410.h, adds a header file created specifically for the UBMP4 circuit board. This file is one of the four files that make up this program, and will be a part of all of our example UBMP4 programs. It contains the #define statements, which name the on-board I/O devices (as described, above), as well as provides function prototypes for some pre-written functions located in the UBMP410.c file.

Interestingly, while the #include and #define directives are parts of your program, and actually provide important information to the compiler, the statements themselves will not become a part of the compiled program. They are simply referenced by the compiler at compile time, and load the proper files or substitute the port pin names when the program is compiled.

The main function

Below the program header is the main program function, shown in the code by the function declaration: int main(void). The main function consists of all of the code within the left-most set of curly braces { } in the code, and has other structures nested within it, inside their own sets of curly braces.

While it may be obvious that all of the code within the curly braces is a part of the main function, not all of the program source code that makes up the main function actually resides in this file. Just as some of the header information is found in other header files, some of the source code for this program is found in other source code files. The first two lines in the main function are actually function call statements which call program function code written as part of the UBMP410.c file.

But, how does the compiler know this? There is no #include directive, like the ones described above, to include the UBMP410.c file! We will find out more about how this happens a bit further on, but for now just remember that the even though the UBMP410.c file is not deliberately included in our code, it is a part of the MPLAB project that was created for this activity.

Take a look at the first two lines of code inside the main function. Each one is, itself, a function call statement that refers to code inside another function.

int main(void)
{
    // Configure oscillator and I/O ports. These functions run once at start-up.
    OSC_config();               // Configure internal oscillator for 48 MHz
    UBMP4_config();             // Configure on-board UBMP4 I/O devices
	
    ...
    
}

Functions inside main

Function call statements call, or run, the program code within the function that matches its name, or its label. Naming functions logically helps to make your program easier to read and understand. As suggested by both the function names and the code comments above, the first function configures the microcontroller’s oscillator, and the second function configures the I/O ports for proper operation in the UBMP4 board.

The empty brackets ( ) at the end of each function call statement indicate that these functions do not need to be provided with any information to do their jobs. Some functions accept variables whose values are passed to them by putting data inside the brackets (we will see an example of data passing to a function later on in the program), but these functions are not meant to be provided with any data.

How do we know whether a function requires data or not, or even that a function exists? Looking through the remaining source code in this program file, we can see no declarations for functions with these names. In fact, the main( ) function is the only function in this source code file.

The answers to the apparently missing functions, as well as the answer to the previous question about how the compiler finds the UBMP4.c file, lie in the UBMP4.h header file. The header file contains both #define directives, as described previously, as well as function prototypes – code that names each function, and lists the data required by the function – for functions contained in the UBMP4.c file. All of the project’s header (.h) and source code (.c) files are stored in the MPLAB project folder. During compilation, MPLAB reads all of the header files included in our program to find data and function declarations, and then looks in the remaining .c files to find all of the required functions. If any are missing, compilation will stop and an error message will appear. If all of the functions are present, the program will be able to be compiled successfully.

We will take a closer look at the code that makes up the OSC_config( ) and UBMP4_config( ) functions in another program activity, so that we can concentrate on learning about input and output code in this program. All of these introductory activities will use these same two functions to configure the microcontroller’s oscillator and I/O circuitry, so you will see them in every example program.

Since these function calls are the first lines of code in the main function, the code inside these functions will be the first code to be run, or executed, by the microcontroller. The code in each function will only run only once, on every power-up or microcontroller reset. After that, the next code to run is the code inside the while loop.

The main while loop runs repeatedly

This a really important part of all microcontroller programs: the main while loop.

    // Code in this while loop runs repeatedly.
    while(1)
	{
      
        ...
        
    }

As you can see from the code comment, the code inside the while loop will run forever, repeating again, and again. Microcontroller programs generally don’t end, or quit, the way computer programs or phone apps do because there is no operating system to take over operation once our program stops running. Think of microcontroller-based devices in the real world. The thing being controlled by a microcontroller (say, a computer mouse) would stop being that thing if it stopped running its program, so it just has to keep on going, running its code forever.

This while structure is in the form of an infinite loop, because the condition in its brackets is set to be 1, equivalent to logically true. The brackets of a while statement normally contain a conditional expression which is evaluated to be either true (1) or false (0). By forcing the expression to be true (by putting the 1 inside the brackets), the contents of the loop repeat forever. The main loop code inside the braces { } under the while statement is the code that will repeat forever.

The flashy part

The next part of the program is, finally, what we set out to do – read inputs and control outputs. It uses the definitions from the UBMP4.h header file to specify the inputs it responds to, and to name the output pins that it controls:

        if(SW2 == 0)
        {
            LED3 = 1;
            __delay_ms(100);
            
            ...
            
        }

The if statement is a conditional structure that evaluates the expression inside its brackets. In this case, the expression reads the state of switch SW2 and compares its value with 0. Comparing two values for equality in C is always performed with two equal signs == as shown in the code. The reason for checking if SW2 is equal to 0 is that the pull-up switch circuits on the UBMP4 will read as a low voltage, or logic 0 when the switches are pressed, instead of being ‘pulled up’ to a high voltage, or logic 1 when the switches are released.

If SW2 is pressed, its level is read as 0, the if statement’s condition becomes logically true, and all of the code inside the curly braces will run – once. The first statement to run when the condition is true turns LED3 on. A single equal sign is used to assign a value, so the statement LED3 = 1; will write a logic 1 to the LED3 output latch, causing the associated microcontroller pin to output 5V, and thereby causing current to flow through LED D3, turning it on.

The next line, __delay_ms(100);, calls a built-in compiler function to produce a 100 ms (0.1 s) time delay. The data inside the brackets (100) is passed to the delay function to specify how long a delay we would like. This delay function is part of MPLAB X, and did not have to be created as part of one of our program files.

The sequence of LED on and off commands and time delays that follow the two example statements are executed in sequence, turning different LEDs on and off, and adding additional delays, until reaching the closing curly brace of the if structure. After writing to all of the LEDs and delaying for all of the delays, the execution of the conditional if structure is complete, and the program continues to the next if statement below it.

But, what if SW2 wasn’t pressed? In this case, the pushbutton state would be 1, the if condition would have be false, and none of the code inside the curly braces would have run. The program would simply skip over the code in the braces belonging to this first if structure, and continue to the next if statement, shown below:

        // Activate bootloader if SW1 is pressed.
        if(SW1 == 0)
        {
            RESET();
        }

This second if statement watches for SW1 to be pressed, and resets the microcontroller using a special reset function if that happens. During a reset, the built-in bootloader code will run (if a bootloader is present) and check for a connection to a computer’s USB port to allow a new program to be written into the microcontroller’s memory.

After evaluating the second if condition, our microcontroller has reached the end of the program code in the while loop. Since the while loop was forced to be true, program execution will resume from the beginning of the while loop, and the program will repeat these same two if conditions forever, making pretty flashing patterns while the SW2 button is held.

Learn more – program analysis activities

Hopefully, you now have a basic understanding of how this program flashes LEDs in response to a button press. Next, we will modify the code and build on the concepts introduced earlier to help deepen your understanding of microcontroller concepts and C language features. A condensed version of the analysis activities below are also embedded in comments at the bottom of the program source code to make it easier for you to copy and paste code directly into the program.

Microcontrollers are fast!

Microcontrollers are fast, but they only do one thing at a time (in this example at least – by using hardware modules, microcontrollers can do more than one thing at a time). Because there are no delays in the code within the while loop itself – the only delay statements are in the first if structure – the program will seemingly respond to our button presses instantly. Try it. See if you can detect a delay between when SW2 is pressed, and when the LEDs begin flashing.

What happens when SW2 is held? Is there a delay between when the LED pattern finishes, and when it restarts? How would you be able to tell?

Try commenting out the last delay in the first if structure (put two slashes // anywhere before the delay in the code, like this: // __delay_ms(100); ). Then, re-compile and re-download the program to UBMP4. Can you see a delay between LED D6 turning off, signifying the end of one pattern, and LED D3 turning on at the start of the next pattern when you press and hold SW2? (You won’t, because the microcontroller is really fast! How fast? Read on.)

The PIC16F1459 microcontroller in UBMP4 has a clock speed of 48MHz, and this is divided by 4 internally to make an instruction cycle time of 12MHz. That means the microcontroller can execute 12 million machine code instructions per second, or one instruction every 83.3ns (nanoseconds, or in 83.3 billionths of a second!). While it actually takes the microcontroller a few instructions to test and potentially skip all of the code in an if condition, as well as a few more instructions to jump from the end of the while loop back to the top, the amount of time taken will be close to 1µs (one microsecond, or a millionth of a second) – unnoticeably short unless you are debugging the program using an oscilloscope or logic analyser.

Ok, so microcontrollers are fast. Now let’s see how they only do one thing at a time. Go ahead and un-comment the last delay (if it is still commented out of the code). What do you think will happen if we press SW2 twice, really quickly, so that both button presses happen in the time it takes to do one complete light flashing cycle? Thinking about how other circuits and devices you have come across work, you might predict one of three possible outcomes:

  • the flashing pattern will start after the first button press, and re-start immediately as SW2 is pressed a second time

  • the flashing pattern will start after the first button press, and go through a second cycle since SW2 was pressed a second time

  • the flashing pattern will start after the first button press, stop when the first cycle is complete, and restart only if the button is pressed after that

Try it! Which did it do? It might be logical to anticipate the correct outcome by looking at the program code and remembering that the microcontroller will execute one instruction after the other, in order. Take another look at the program code to relate it to the following explanation.

While the order of instructions in a program might change when the microcontroller comes across a a conditional instruction, or a loop, the order of instructions will not change while running the code inside the condition or loop structure (inside the curly braces { } ). Since the conditional check only happens once at the beginning of the loop, when the code enters the loop, it continues through all of the instruction inside the loop until the very end of the loop. If there are no other conditional checks inside the loop, the microcontroller will not know if the state of the input (SW2 in this case) has changed.

So, after examining the code, it is important to understand that first two possibilities cannot occur. This is because once the microcontroller has determined that SW2 is pressed and enters the code inside the first if structure, there is no code within that if structure to either re-evaluate SW2 or to count how many times it was pressed. The microcontroller simply follows through each line of the code, turning LEDs on, waiting for each delay, and turning LEDs off, and while it is busy doing that, it cannot know if the state of the switch has changed. The only possibility is the last one. Each button press starts a cycle of flashing the LEDs, and only when that is complete can the microcontroller check for a new button press.

Assignment and conditional operators

In the program, the statement LED3 = 1; uses the equal sign as an assignment operator. The value 1 is assigned to the port pin referenced by LED3, setting its output high and causing LED3 to turn on.

The statement if(SW2 == 0) uses two equal signs as a conditional operator instead of one. Why is that? It would seem that wording the conditional statement with a single equal sign would make just as much sense, and be easier for programmers to remember while coding, than having to use two equal signs.

The answer to this goes back to the early days of programming languages. While the statement if(SW2 = 1) makes perfect sense to us as a comparison between the SW2 input and logical level 1, the operation inside the brackets (SW2 = 1) is typically used to assign the value of logical 1 to SW2. A computer would have to decide when an equal sign means we want to assign a value, and when the equal sign means we want to use it to compare values instead. Getting the computer to make this decision would require extra code, making the code in the compiler more complex, larger, and slower. And, in the early days of computers, with their slow processors and limited memory, that would not have been a desirable option when a simpler solution exists. The simpler solution is to ask the code writer to decide – the programmer knows what they intend to do – and to use two equal signs to compare values, or a single equal sign to assign values.

Conditional operators

Conditional expressions in C can use the double equal sign equality operator (==), or any of the other conditional operators, below. These can be used in the condition of if-statements and well as while statements.

Operator Condition
== equal to
!= not equal to
> greater than
>= greater or equal to
< less than
<= less or equal to

I/O pins and ports

The statement LED3 = 1; turns on an output pin. How do you know which pin it is?

Remember that LED3 is the reference name of one of the components on the UBMP4 circuit board. LED D3 is connected to a physical pin on the PIC16F1459 microcontroller. The UBMP4 schematic is a quick way to find the pin that connects to LED D3.

LED3 is one of the names that defines LED D3 in our program. This software name specifies both the RAM register address, and LED D3’s bit position within the RAM register, and is found in the UBMP4.h header file. Remember, too, that although we often think of an I/O pin as being attached to one specific port, many more registers are actually involved – PORTC, LATC, TRISC, and ANSELC in this case. In the UBMP4’s microcontroller, writing a value to LED D3 is done through one specific bit of the LATC RAM register.

The program code, below, uses LATC instructions that write to the all of the bits of the PORTC output latches directly. Try copying and pasting this code below the existing SW2 if structure, at the location shown by the comment. After compiling the code and downloading it into UBMP4, you should see all of the LEDs flashing when you press and hold SW3.

        if(SW3 == 0)
        {
            LATC = 0b00000000;
            __delay_ms(100);
            LATC = 0b11110000;
            __delay_ms(100);
        }

Controlling all of the PORTC pins using a single LATC assignment statement is easier for us to do because it takes less code than using for separate LEDx = 1 statements, as shown in the original program. Since each of the four LEDx = 1 statements compiles to one machine code instruction, and each of these machine code instructions would have to execute sequentially, this code, using just one machine code instruction, also results in smaller code and faster code execution.

If this method of writing to all of the PORT pins at once produces simpler, smaller, and faster code, why wouldn’t we use all of the time? One reason is that the LATC command really does over-write all eight pins of the PORT. The four UBMP4 LEDs comprise four of the eight pins of PORTC. What are the other pins of PORTC used for? Without knowing what they are connected to, and, more importantly, what their existing states were, the LATC instruction blindly over-writes them to be zero in this example, which may not be the output state they were before, or the state we want them to remain in. So, LATC commands like this have to be used carefully.

In a future activity, we will look at logical bit operations to selectively change specific bits within a PORT. For now though, realize that LATx operations are powerful because they have the ability to change all the bits you want changed in a single command, as well as dangerous because they will also potentially change the bits you may not have wanted to change.

Comparing if and while

When an if condition in a program is true, the code immediately following the if statement is executed. The code after the if statement is usually grouped within curly braces, although a single line statement is also permitted. For safety, it is a good idea to always use curly braces, as any lines added after the first line always execute, regardless of whether the if condition is true or false, if the curly braces are omitted. For example, while it looks like LED3 and LED4 should only turn on if SW3 is pressed, the code in this example always turns LED4 on, regardless of the state of SW3:

if(SW3 == 0)
  LED3 = 1;		// Turns on only if SW3 is pressed
  LED4 = 1;		// Always turns on

To ensure both LEDs are controlled by SW3, ensure both statements are grouped inside curly braces:

if(SW3 == 0)
{
  LED3 = 1;		// Turns both LEDs on if SW3 is pressed
  LED4 = 1;
}

While conditions can be used just like if conditions, with the difference being that the code immediately below the while statement will be repeated as long as the condition remains true. Replace the code that you added for SW3, above, with this code:

        // Momentary button using if structure
        if(SW3 == 0)
        {
            LED4 = 1;
        }
        else
        {
            LED4 = 0;
        }

        // Momentary button using while structure
        while(SW4 == 0)
        {
            LED5 = 1;
        }
        LED5 = 0;

To see the difference in operation between the if statement and the while loop, try pressing and releasing SW3 and SW4 one at a time, first. Pressing each button should light its LED.

Next, press and hold SW3 while pressing and releasing SW4. It should work similarly to before – each LED should light when its pushbutton is pressed, and in this case LED4 will stay lit while LED5 turns on.

Next, try press and holding SW4 while pressing and releasing SW3. You will notice that LED4 does not turn on when SW3 is pressed. This is because pressing SW4 keeps the while condition true, and the while loop code keeps alternating between lighting LED5 and checking SW4. Only after SW4 is released, and while the condition is false, will the microcontroller be able to exit the SW4 while loop to check SW3.

Logical AND conditions

Nesting if statements creates a logical AND operation – both the first condition as well as the nested, condition inside it must be true for the code within the inner condition to run. We can make a logical AND condition using the program code, below. LED D4 will only light if both SW3 and SW4 are pressed simultaneously.

        // Nested if 'AND' code
        if(SW3 == 0)
        {
            if(SW4 == 0)
            {
                LED4 = 1;
            }
            else
            {
                LED4 = 0;
            }
        }
        else
        {
            LED4 = 0;
        }

Test the code to ensure it works as expected. Does the order of the if conditions matter? It shouldn’t, as both conditions have to be true for the code inside the second condition to run. You can think of these two if conditions as the software equivalent of two switches in series – current will only flow through the series circuit to the load if both switches are closed.

Instead of nesting if conditions, a logical AND operator within the if condition can make the code more compact, especially when a number of conditions are combined in the if statement. A logical AND is composed of two ampersands && as shown in the code below. Try it by replacing the code, above, with this new code, and verify that it works the same way:

        // Conditional 'AND' code
        if(SW3 == 0 && SW4 == 0)
        {
            LED4 = 1;
        }
        else
        {
            LED4 = 0;
        }

Logical OR conditions

Replace the double ampersand && with double vertical bars || to make a logical OR conditional operator. Your code should look like this:

        // Conditional 'OR' code
        if(SW3 == 0 || SW4 == 0)
        {
            LED4 = 1;
        }
        else
        {
            LED4 = 0;
        }

Using the OR operation, pressing either SW3 or SW4, or both, will turn LED 4 on. You can think of an OR operation as the equivalent of a parallel circuit – pressing either parallel switch will allow current to flow through the LED.

There is one more logical operator that is less commonly used than AND and OR in this type of input program, and that is the XOR operator. XOR represents the exclusive-OR condition, which would turn on the LED if SW3 or SW4, but not both, were pressed. An XOR operation is made by using two caret symbols ^^ as the conditional operator in the condition statement. This table summarizes all of the logical operators:

Operator Operation
&& logical AND
|| logical OR
^^ logical XOR

Activity 1 learning summary

Programs are composed of one or more files. Header files contain information about the program, its functions, register names, and constants. Source files contain program instruction statements and function routines. The main( ) function is the beginning of a C language program, and typically contains functions or instructions that will run once, followed by instructions in a loop that will repeat forever.

Input/output (I/O) circuits are connected to microcontroller pins, and the state of those pins is read or written by program instructions through RAM registers inside the microcontroller. I/O pins can be set to be either inputs or outputs, but cannot be both simultaneously. I/O pins can be read or written individually, or in groups by their I/O port.

Conditional statements allow programs to make decisions based on inputs. If statements evaluate single or multiple logical conditions once to determine whether or not to perform another action. While statements evaluate their conditions repeatedly, looping program instructions until the condition becomes untrue.

Logical operators such as AND, OR, and NOT can join multiple conditions to evaluate more complex conditional combinations.

Activity 1 Programming Challenges

1. The statement __delay_ms(100); creates a 100ms delay. Try changing one or more of the delay values in the program to 500ms and see what happens.

Can the delay be made even longer? Try 1000 ms. How big can the delay be before MPLAB-X produces an error message? (Hint: can you think of a fast and efficient way of guessing an unknown number?)

2. The __delay_ms( ); function only accepts whole-number integers as delay values. To make delays shorter than 1ms, use the __delay_us( ); function to specify a delay in microseconds instead. You won't be able to see such short LED flashes with your eyes, but you could measure them using an oscilloscope, or hear them if they are used to turn the piezo beeper on and off. Try this code:

        // Make a tone while SW5 is held
        if(SW5 == 0)
        {
            BEEPER = 1;
            __delay_us(567);
            BEEPER = 0;
            __delay_us(567);
        }

Try changing the delay values in the __delay_us( ); functions, ensuring both delay values are identical. Does the pitch of the tone go lower or higher if the delay value is made smaller? Why do you think this is?

3. This code demonstrates a more compact way of toggling the beeper output using a logical NOT operator ( ! ). Replace the code above, with this code:

        // Make a tone while SW5 is held
        if(SW5 == 0)
        {
            BEEPER = !BEEPER;
            __delay_us(567);
        }

One difference between this code and the code in 2, above, is the state of the BEEPER pin when SW5 is released. What state will the BEEPER output be in after this code runs? While one advantage of this method is smaller code, can you think of one or more disadvantages based on its output when the button is released?

4. Using modified versions of the original SW2 if structure, create a program that makes a unique LED flashing pattern for each pushbutton.

Test each of your flashing patterns. Describe what happens when more than one button is held. Do all of the patterns try to flash the LEDs at the same time, or sequentially? Explain why this is.

5. Create a program that makes a different tone for each pushbutton.

Test each tone by pressing each button individually. Next, press two or more buttons at the same time. Describe what the tone waveform would look like when more than one button is held.

6. Use individual if structures to simulate Start and Stop buttons for an industrial machine. LED D4 should turn on when SW3 is pressed, stay on even after SW3 is released, and turn off when SW4 is pressed. Test your program to make sure it works.

7. Running your program from 6, above, describe what happens when both SW3 and SW4 are pressed. Does LED D4 stay on? If so, how does the brightness of LED D4 compare between its normal on state following SW3 being pressed to this new state when both SW3 and SW4 are bing held? Can you explain why it changes?

8. As you can imagine, an industrial machine that is able to turn on even while its Stop button is pressed represents a significant safety hazard. Using a logical conditional operator, modify the start-stop program from activity 5 to make it safer. SW3 should only be able to turn on LED D4 if SW4 is released.

9. LED D1 is normally used to indicate that a program is running, but it can be controlled by your program as well. If you take a look at the UBMP4 schematic, you will see that LED D1's cathode (or negative) pin is connected to the microcontroller instead of the anode (positive) pin as with the other LEDs. This means that you need to make D1's output a zero to turn D1 on. Try it! Make a program that controls or flashes LED D1.

 

UBMP4 Introductory Programming Activity 1 - Input-Output Program