UBMP4 Introductory Programming Activity 1

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 to ensure that everything is working.

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 start a new program project in an IDE (Integrated Development Environment). An IDE manages all of the files and settings for a 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 any PICkit-style programmer without making use of the bootloader.

Using the bootloader makes the process of programming the microcontroller faster and 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 method 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 drive named PIC16F1459. Or, if using MPLAB Xpress, download the .hex file and then copy the downloaded file from your computer’s Downloads folder onto the drive named PIC16F1459.

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 and on, and 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. To reprogram UBMP4 with a new program, press and release pushbutton SW1. Doing so will blink LED D1 off and on, and start the bootloader. UBMP4 will then be mounted on your computer as a drive named 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 the program in your UBMP4 contains the code required 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, described next.

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 the 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 a 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 ignored by the compiler and start 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 and circuits 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 accessed using its part reference name as labelled on the UBMP4 circuit itself. All of these reference names are defined in the UBMP420.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 circuit components in the program this way is very convenient, since it allows individual devices to be controlled using only their reference names printed on the circuit board. There is no need for us to look up which I/O pin the component connects to on the schematic. For every one of these programming assignments, we can simply refer to each part by its part reference name.

Controlling multiple I/O pins at the same time, or changing the characteristics of the port pins, 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 up to eight pins which are then 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 for I/O pins, since that would require 24 pins and the PIC16F1459 only has 20, which includes pins reserved for power, ground, USB data, and the USB voltage regulator. 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 0, or pin 0, of the port. Checking the UBMP4 schematic, you will see that LED D2 connects through resistor R7b to the 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 UBMP420.h MPLAB project file has already made the association between the pin names and part reference names for us. If you open the UBMP420.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 D2          LATCbits.LATC4  // LED D2 output
#define LED2        LATCbits.LATC4  // LED D2 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 D2 and LED2 both refer to the I/O pin LATCbits.LATC4. It is defined twice for convenience – in this case the name D2 to represent LED D2 on the newer UBMP4 boards, while the same LED’s part reference was LED2 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 UBMP420.h file can be edited to change or add additional I/O pin definitions, as needed.

You might have noticed that D2 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 the microncontroller’s internal 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, PORTC is the address of the internal RAM register that holds the values (0 or 1) of each of the PORTC I/O pins. In newer microcontrollers, 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 a single output device, such as LATC4, writes only the signle bit of data into the latch location associated with that bit, which in this case would be the RC4 pin, or D2 on UBMP4.

  • TRISC is the tristate control register for the port, and controls which of the port C pins will be used as input or output pins. Writing 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 0 being like the ‘O’ in output, and 1 being like the ‘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 latch (LATA and LATB), tristate (TRISA and TRISB), and analog (ANSELA and ANSELB) registers. PORTA and PORTB also have two extra registers used to control internal weak-pull-up resistors (which are not part of the PORTC hardware), 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 all circuit inputs will have a high electrical impedance on start-up and will hopefully not inadvertently activate output devices before the software has configured the I/O pins to their proper levels.

Before using an I/O port, our software will need to configure each of the ports using their ANSELx, TRISx, and WPUx registers. We will see exactly how port configuration is accomplished in a future program (but you can explore the port configuration function in the UBMP420.c file to see the process). In this activity, we will 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, and all of the UBMP4 programs will contain at least these four files. Splitting the program into multiple files makes it easier for us to get to the important parts quickly and it provides more flexibility for future software or hardware modifications than by writing the program as a single, long file. The function of each of these 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.

  • UBMP420.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.

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

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

The main Intro-1-Input-Output.c file

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          Activity: mirobo.tech/ubmp4-intro-1
 Date:    May 9, 2023
 
 This introductory input and output programming activity for the mirobo.tech
 UBMP4 demonstrates pushbutton input, LED (bit) output, port (byte) output,
 MPLAB's built-in time delay functions, and simple 'if' condition structures.
 
 Additional program analysis and programming activities demonstrate the logical
 AND and OR conditional operators, the use of time delay functions to create
 sound output, and software-based 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    "UBMP420.h"         // Include UBMP4.2 constants and functions

// 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)
        {
            LED2 = 1;
            __delay_ms(100);
            LED3 = 1;
            __delay_ms(100);
            LED4 = 1;
            __delay_ms(100);
            LED5 = 1;
            __delay_ms(100);
            LED2 = 0;
            __delay_ms(100);
            LED3 = 0;
            __delay_ms(100);
            LED4 = 0;
            __delay_ms(100);
            LED5 = 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, containing code comments, include statements, and often function, constant, and variable declarations, is usually 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, but comment blocks are more convenient for commenting out multiple lines of code.

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, most of the header information in this program is actually located in a separate header file, UBMP420.h.

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

#include    "UBMP420.h"         // Include UBMP4.2 constants and functions

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 source code (though there are ways to find it).

The last include statement, #include UBMP420.h, adds a header file created specifically for the UBMP4.2 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.2 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 UBMP420.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 do 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. In contrast, the majority of the remainder of the program will be compiled into the machine code that runs in the microcontroller.

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 – we’ll call it main( ) – consists of all of the code within the left-most set of curly braces { } in the code – from the top opening brace {, right below the int main(void) declaration, to the bottom closing brace }, the last of the three closing braces in a row. The position of these braces shows a code hierarchy, with all of the rest of the program structures nested within the main( ) function.

While it may be obvious that all of the code within the main( ) function’s curly braces is a part of the main function, not all of the program source code in 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 on the program function code written in the UBMP420.c file.

But, how would the compiler know this? There is no #include directive, like the one for the header file described above, to include the UBMP420.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 UBMP420.c file is not deliberately included in our code, it is a part of the MPLAB project that was created for this programming activity, and MPLAB uses all of the files to make our program.

Take a look at the first two lines of code (after the comment – // ) inside the main function. Each one is itself a function call statement that refers to code inside another function, instead of being an actual program expression.

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 passing data to a function later on in the program), but these two functions are not designed to receive any data.

But wait, how do we know whether a function requires data, or that a function even exists? Looking through the remaining source code in this program file, we will not find any declarations for these functions. 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 question posed above relating to how the compiler finds the UBMP420.c file, lie in the UBMP420.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 UBMP420.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 all of the project’s .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 programming activity, so that we can concentrate on learning about the input and output code in this program. Each of the UBMP4 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 you make.

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 instructions to run come from the code inside the while loop.

The main while loop runs repeatedly

This a really, really important part of all microcontroller programs – the main while loop. (Did we emphasize that enough?)

    // 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 the operation of the microcontroller once our program stops running. Let’s use an example of a microcontroller-based device in the real world – a computer mouse. The mouse can only act like a mouse while its program is running. Stopping the mouse program from running would stop the mouse from being a mouse, so it just has to keep on going, running its mouse code in a loop, forever.

This while structure is in the form of an infinite loop, one that goes on forever, because the condition inside its brackets is set to be 1 (equivalent to logically true). The brackets of a while statement normally contain a conditional expression that is evaluated to be either true (1) or false (0) at run time. For example, the expression while(total < 10) would be true (or 1) if the value of total is less than 10, and fast (or 0) if the value of total is 10 or more. Forcing the expression to be true by putting the 1 inside the brackets makes the contents of the loop repeat forever. What are the contents of the loop? All of the code that is grouped inside the next set of matching braces { }, under the while statement, which, in this program, is all of the rest of the code.

The flashy part

Inside the main while loop of this program is, finally, what this program set out to do – read inputs and control outputs. It uses the definitions from the UBMP420.h header file to specify the inputs it responds to, and to name the output pins that it controls. Because these names specify the components on UBMP4, it’s pretty straightforward to figure it out:

        if(SW2 == 0)
        {
            LED2 = 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 to 0. Comparing two values for equality is always performed using two equal signs == in C (as well as in many other programming languages). The reason for checking if SW2 is equal to 0 is that the pull-up switch circuits on the UBMP4 will cause the pins to be 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, and the if statement’s condition becomes logically true. When this happens, all of the code inside the next set of curly braces below it will run – once. The first statement to run when the condition is true turns LED2 on. A single equal sign is used to assign a value, so the statement LED2 = 1; will write a logic 1 to the LED2 output latch, causing the associated microcontroller pin to output 5V, and thereby causing current to flow through LED D2, 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 statement below its closing brace.

But, what if SW2 is not pressed? In this case, the pushbutton’s state would be 1, the if condition would be false, and none of the code inside the curly braces will run. The program would simply skip over the code inside the braces belonging to this first if structure, and continue to the next if statement, located below the closing brace of the first one in the program, and 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 the bootloader program 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 reaches the end of the program code in the main while loop when it reaches the while loop’s closing brace. Since the while loop was forced to be true, program execution will resume from the beginning of the while loop, causing the program to repeat these same two if conditions forever, checking SW2 and SW1 repeatedly, and making pretty flashing patterns whenever button SW2 is pressed!

Learn more – program analysis activities

Hooray, 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 is also embedded in the comment block at the bottom of the program source code to make it easier for you to copy and paste the example code directly into the program. You can add your code to the program at the location shown by the comment located between the two if structures in the main part of the program.

Microcontrollers are fast!

Microcontrollers are fast, but they only do one thing at a time – in this example at least. (Some microcontrollers can actually do more than one thing at a time by using hardware modules for certain functions.) Because there are no delays in the code within the while loop itself – outside of the delay statements 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 in the line ahead of the delay function, like this: // __delay_ms(100); ). Then, re-compile and re-download the program to UBMP4. Can you see a delay between LED D5 turning off, signifying the end of one pattern, and LED D2 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 is it? Read on.

The PIC16F1459 microcontroller used in UBMP4 is configured to use a clock speed of 48 MHz, and this frequency is divided by 4 internally in the microcontroller to make an instruction cycle time of 12 MHz. This means that the microcontroller in UBMP4 can actually execute 12 million machine code instructions per second, or one instruction every 83.3 ns (0.000 000 083 3 s, or 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 to run one complete while loop whenever no buttons are pressed will be around 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 now you know 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) to return the code to its original state. What do you think will happen if we press SW2 twice, really quickly, so that both button presses happen during the time it takes to do one complete light flashing pattern? Thinking about how other circuits and devices you have come across work, you might predict there will be one of three possible outcomes:

  • the flashing pattern will start after the first button press, and if SW2 is pressed again the pattern will re-start by lighting LED2 immediately and not finish the first pattern

  • the flashing pattern will start after the first button press, completing the whole first light pattern, and then start a second pattern since SW2 was pressed twice

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

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. Look at the program code again, to help you relate it to the following explanation.

While the order of groups of instructions (inside curly braces) in a program might change whenever the microcontroller runs a conditional instruction, the order of instructions within the active block of a program cannot change. Since the conditional check for SW2 being pressed only happens once, at the start of the loop, all of the instructions inside the loop will run in succession until the end of the loop whenever initial condition is true. Since there are no other conditional checks to examine the state of SW2 inside the loop, the microcontroller will not know that its state has changed until the next time the if condition statement runs. The if condition only checks the state of SW2 when it runs, it has no way of knowing the state of SW2 at any other time.

So, after examining the code, it is important to understand that first two possibilities cannot occur. 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’s state, or to count how many times it was pressed. The microcontroller simply follows through each line of the code, turning each LED on, waiting for a delay, and turning each LED off. While it is busy doing that, it cannot know if the state of the switch has changed. The only possibility is the last one. Only a new button press after the end of a flashing cycle can start a new flashing cycle of the LEDs.

Assignment and conditional operators

In the program, the statement LED2 = 1; uses the equal sign as an assignment operator. The value 1 is assigned to the port pin referenced by LED D2, setting its output high and causing LED D2 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 humans 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 was just not a desirable option when a simpler solution exists. The simpler solution was to just ask the code author to decide – after all, the programmer should know what they intend to do – and to tell the computer to compare values by using two equal signs, or to assign a value using a single equal sign.

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 within the conditional expression brackets of both if-statements and while statements in any program.

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 LED2 = 1; turns on an output pin. How do you know which pin it is?

Remember that LED2 is the reference name of one of the components on the UBMP4 circuit board. LED D2 is connected to a physical pin on the PIC16F1459 microcontroller. The UBMP4 schematic is one way to check the physical pin on the chip, and looking up LED2 in the UBMP420.h file is another way to determine the microcontroller pin name.

LED2 is one of the names that defines LED D2 in our program. This software name specifies both the RAM register address, and LED D2’s bit position within the RAM register, and is found in the UBMP420.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 D2 is done through one specific bit of the LATC RAM register.

The program code, below, uses LATC expression to write all eight bits of the PORTC output latches directly, instead of writing one bit at a time. Try copying and pasting this code below the existing SW2 if structure, at the location shown by the comment in the main section of the code. After compiling the code and downloading it into UBMP4, you should see all of the LEDs flashing simultaneously when you press and hold SW3.

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

Controlling multiple PORTC pins using a single LATC assignment statement is easier and takes less code than using separate statements to turn on the LEDs as was done in the original program. To perform this equivalent operation the way we did before would require four separate LEDx = 1 statements. Since each statement will compile to one machine code instruction, and each of the four machine code instructions would have to execute sequentially, this code, using just one machine code instruction, 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 it 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, but 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 will blindly over-write them to be zero (in this example). Zero may not be the output state they were in before, or the state we want them to remain in now, so this command may cause unexpected results on any devices that happen to be connected to those other pins. LATC commands are fast and powerful, but have to be used carefully (remember the rule: with great power comes great responsibility!).

In activity 5, 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 inside a pair of curly braces, although a single line statement without curly braces is also permitted. For predictability and consistency, it is a good idea to always use curly braces. This is because any lines of code added below the first line of the if statement will always execute (regardless of whether the if condition is true or false!) if the curly braces are omitted. For example, while the code below is indented so it looks like LED3 and LED4 should only turn on if SW3 is pressed, when the code runs LED4 is always turned 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, group both statements inside curly braces:

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

While conditions can be used in the same way as if conditions, with the important difference being that the code immediately below the while statement will be repeated as long as the condition remains true. Try adding the following code to your program, below the other if conditions:

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

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

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

Next, press and hold SW4 while pressing and releasing SW5. 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 SW5 while pressing and releasing SW4. You will notice that LED4 does not turn on at all when SW4 is pressed. Can you figure out why from the code? Pressing SW5 keeps the while condition true, and the microcontroller keeps running the code inside the while loop, alternating between lighting LED5 and checking SW5. Only after SW5 is released, when the while the condition is false, will the microcontroller be able to check the SW4 if statement.

As a result, use separate if conditions in your code if your program needs to respond to multiple conditions quickly.

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 inside the inner condition to run. Try to make a logical AND condition using the program code, below (make sure you replace the if and while code if you added it in the step above). LED D4 will only light if both SW4 and SW5 are pressed simultaneously.

        // Nested if 'AND' code
        if(SW4 == 0)
        {
            if(SW5 == 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. In contrast, two separate if conditions act like separate switch circuits wired in parallel.

Instead of nesting the conditions, a logical AND operator within the conditional statement can make the code more compact, especially when a number of conditions are to be combined in an 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(SW4 == 0 && SW5 == 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(SW4 == 0 || SW5 == 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 another kind of parallel circuit – two switches wired in parallel to a single load. Pressing either pushbutton will allow current to light 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 either SW4 or SW5, but not both, was pressed. An XOR operation is made by using two caret symbols ^^ in some programming languages but this operator is not supported in C. Since an XOR operation represents only one of the inputs being true, and not the other, it can be replaced by the not equal to operator, !=. This table summarizes all of the logical operators:

Operator Operation
&& logical AND
|| logical OR
!= logical XOR

Activity 1 learning summary

C language programs are often composed of one or more source code 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. The separate parts of programs and program structures are grouped inside nested pairs of curly braces, and it’s important to maintain their hierarchy when creating and editing the program code.

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 inputs and outputs simultaneously. Some I/O pins have additional capabilities such as analog input, or software-controlled pull-up resistors. I/O port pins can be read or written individually, or in groups by using I/O port instructions.

Conditional statements allow programs to make decisions based on inputs. If statements evaluate single or logically grouped 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, or false.

Logical operators such as AND and OR and commonly used 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 get 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 accepts only integers as delay values. To create delays shorter than 1 ms, a different function must be used. Use the '__delay_us();' function to specify delays in microseconds.

You won't be able to see microsecond length LED flashes with your eyes, but you can measure them using an oscilloscope, or hear them if they are used to turn a piezo beeper on and off instead. Try the following code in your program:

        // 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 both of the __delay_us(); functions. Does the pitch of the tone increase or decrease if the delay value is made smaller? Does the opposite happen if the delay is made larger?

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 activity 2, above, is the state of the BEEPER pin when SW5 is released. Will you know the state the BEEPER output will be in after this code runs? While one advantage of this method is less program code, can you think of one or more disadvantages of not knowing the output state of a pin when using code like this?

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 so.

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 you think the tone waveforms will look like when two or more buttons are held. (You can verify your prediction if you have access to an oscilloscope.)

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

7. Running your program from activity 6, above, describe what happens when you both SW3 and SW4 are held. Does LED D3 stay on? If so, how does its brightness compare to when only SW3 is pressed and released? If it is different, can you explain what part of the code causes it to change, and why it changes?

8. As you might imagine, an industrial machine that is able to turn on even while its 'Stop' button is pressed represents a significant safety hazard. Using one or more of the logical conditional operators introduced in the analysis activities, above, modify your start-stop program to make it safer. SW3 should only turn LED D3 on if switch SW4 is not being pressed while switch SW3 is pressed.

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 examine the CHRP4 schematic, you will see that LED D1's cathode (or negative) pin is connected to the microcontroller instead of having its anode (positive) pin connected to the microcontroller as the other LEDs do. This means that the LED D1 output must be made equal to 0 to turn D1 on.

Try it! Make a program that controls or flashes LED D1 as part of a light pattern. (Be careful that you don't change the SW1 reset code when making your light pattern, or you may not be able to use switch SW1 to reset the board and enter programming mode using the bootloader. If this happens, unplug the USB cable and press and hold SW1 while re-connecting the USB cable. LED D1 will remain off until you release SW1, and then the bootloader will start. Then, be sure to re-enable the SW1 reset code that your code edits accidentally disabled.)

 

UBMP4 Introductory Programming Activity 1 - Input-Output Program