UBMP4.1 Introductory Programming Activity 2

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

Variables and constants

The Intro-1-Input-Output program demonstrated how to read inputs and control outputs. In that activity, the actions of reading the pushbutton inputs and lighting the corresponding LED outputs happened immediately. But, what if you wanted to wait for a number of input events before activating the output, or repeat the output actions a number of times instead? One way to do this is with a software variable – they enable your program to keep track of things by storing a count in the memory registers of a computer or microcontroller.

Computer number systems and codes

The PIC16F1459 is an 8-bit microcontroller, so it stores numbers as eight binary bits in its internal memory registers. Using all eight bits, a total 256 different states can be stored, each represented by a unique combination of zeros and ones known as a code. Selected numbers from this sequence of codes, which consists of every possible combination of zeros and ones between 00000000 and 11111111, are shown in the left column of the table, below:

8-bit Unsigned Signed ASCII Binary Decimal Decimal Character Control Code Equivalent Equivalent Code Code ---------- ---------- ---------- ---------- ---------- 11111111 255 -1 nbsp 11111110 254 -2 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 10000000 128 -128 Ç 01111111 127 127 DEL ... ... ... ... ... 01100010 98 98 b 01100001 97 97 a ... ... ... ... ... 01000010 66 66 B 01000001 65 65 A ... ... ... ... ... 00110001 49 49 1 Ch-1 00110000 48 48 0 Ch-0 ... ... ... ... ... 00000001 1 1 SOH Play 00000000 0 0 NUL Stop

The left column of numbers are arranged in descending order according to the binary number code. Each binary number’s decimal equivalent is shown in the second column of the chart, labelled Unsigned Decimal Equivalent. In binary, each bit position represents a value based on a power of 2 (just as each digit of a decimal number represents a value based on a power of ten), so these binary codes represent all of the numbers between 255 and 0. But, it is important to realize is that encoding the positive numbers up to 255 is not the only way of interpreting these binary codes.

For instance, some computer uses might need both positive and negative numbers. If negative numbers are required, a microcontroller cannot simply write a minus sign (-) in front of a number as we would with decimal numbers. This is because each bit position of the memory register can hold either the digit 0 or the digit 1, leaving no way to represent a minus sign (since the minus sign would represent a third digit value, or adding an extra digit to the number).

So, how can computers represent negative numbers? Some clever thinkers simply decided that the minus sign could be represented using one of the digits we already have, and chose the digit 1 to represent the minus sign. It’s actually quite ingenious in its simplicity!

Only the very first digit of a binary number can be used as a minus sign, becoming known as the sign bit. Since the sign bit is the most significant bit of a binary number, it splits the range of numbers into an equal number of positive and negative values, known as signed binary numbers. Some of the signed binary values are shown in the third column of the chart, labelled Signed Decimal Equivalent.

Note that there is no difference in the actual binary codes that represent positive numbers greater than 127 and the negative numbers between -1 and -128. The difference is only in how the numbers are interpreted. This means that if you decided to use the binary number 11111111 to represent -1 instead of 255, that would be perfectly fine. But, if you want the microcontroller to understand that you are using signed binary code, you need to tell it so.

Programmers have created codes for common uses of binary numbers, such as the ones listed in the second, third, and fourth columns of the table, above. These represent three very commonly used 8-bit codes: unsigned numbers (all positive), signed numbers (half positive, half negative), and the ASCII character code (an early and still commonly used character code for primarily Latin-alphabet languages, like English).

The last column of the chart represents a unique control code, perhaps created for a TV streaming device remote control protocol. Specific codes are assigned to represent each button on the remote control, and as long as the remote control transmitter and receive agree on the meaning of the codes, the devices will be able to communicate successfully. Custom codes like this can be created for any purpose, but will be generally be unique to a particular device or brand of devices.

Programmers have created codes for other common sizes of binary numbers, in both unsigned and signed formats. These codes are known as numeric types, and they will be used by our programs to let the compiler know how to represent our variables.

 

New Concepts

All digital computers represent numbers using groups of bits. Here are some things you should know if you’re new to this:

  • bit - short for binary digit, a bit is a single memory location that stores one of two oppositely charged states, which we refer to as being either zero (0), or one (1)

  • byte - a group of 8 bits which can represent one of 256 different states (combinations of zeros and ones) as either a number or a code

  • nybble (or nibble) - a group of 4 bits commonly used to represent decimal or hexadecimal number codes

  • word - a group of more than 8 bits (often 16, but can be other lengths) which can represent one of 65 536 different states

  • bool (boolean) - a 1-bit value typically used to represent a logical state (false/true) or binary value (0/1)

  • char (character) - an 8-bit value typically used to represent a number from 0-255, or an ASCII character

  • int (integer) - a 16-bit value typically used to represent a number from 0-65 535

  • long (long integer) - a 32-bit value used to represent a number from 0-4 294 967 296

  • sign bit - the most significant bit of a number which indicates that the number represents a negative value if the sign bit is set to one (1)

  • code - a specific combination of bits representing a quantity, symbol, or signal

  • binary code - a code using only the binary digits 0 and 1 to represent numbers

  • hexadecimal code - a code using the hexadecimal digits 0-9 and A-F to represent numbers

  • ASCII code - a code using 8 binary bits to represent common latin characters, symbols, and control signals

  • BCD (binary coded decimal) - a code using binary to represent the decimal digits 0-9

Numeric types for programming

The following chart shows the most common numeric types, their bit lengths, and the lowest and highest values that each type can represent:

Type Bits Lowest Value Highest Value Used for:
bool (Boolean) 1 0 (false) 1 (true) Logic, input and output bits, flags
nybble 4 0 15 1 Hex digit, 1 BCD digit
unsigned char 8 0 255 Small positive integers, ASCII characters (primary data type for 8-bit PIC microcontrollers)
signed char 8 -128 127 Small positive and negative integers
unsigned int (integer) 16 0 65 535 Large positive integers, variables
int (signed integer) 16 -32 768 32 767 Large positive and negative integers (primary data type for Arduino)
unsigned long 32 0 4 294 967 295 Very large positive integers, variables
long (signed long) 32 -2 147 483 648 2 147 483 647 Very large positive and negative integers
float 32 -3.4028235E+38 3.4028235E+38 Very large positive and negative decimals

Remembering that the PIC16F1459 is an 8-bit microcontroller, you would guess that it can easily represent Boolean numbers, nybbles, and both unsigned and signed characters. It can actually represent all of the numeric types, including large, 32-bit numbers. But how? The simple answer is that is does it by separating the number into 8-bit chunks, and working with one 8-bit chunk at a time.

The downside to using numbers larger than 8 bits is that they require a separate 8-bit memory register to hold each 8-bit piece of the number. And, performing math operations on larger numbers has to be done in a way that keeps track of not only the changes within each 8-bit piece of each number, and that result, but also all of the changes that might ripple through to affect the other 8-bit portions of the results. Doing all of this extra tracking requires extra program instructions, and running all of the extra instructions repeatedly on each 8-bit portion of the numbers takes extra time.

Using the proper numeric type for each task will result in smaller, more efficient program code and faster performance. If a loop needs to run 100 times for example, an unsigned character (which can represent numbers from 0-255) has more than enough states to count the loop cycles and can do so much more efficiently than using a larger number type such as an int. In C, unlike in some newer programming languages, the programmer (that’s you) is given the responsibility of choosing the correct numeric type for each task.

The main program

Download the Intro-2-Variables program files (.zip) to create the MPLAB project for this activity, or import the files into MPLAB from its Github repository. The contents of the main file, Intro-2-Variables.c, is shown below.

/*==============================================================================
 Project: Intro-2-Variables
 Date:    March 1, 2022
 
 This example program demonstrates the use of byte (char) constants and
 variables to count button presses and trigger actions when a limit is reached.
  
 Additional program analysis and programming activities demonstrate using bit
 (Boolean, or bool) variables to store state for operations such as preventing 
 multiple counting of a singe button press during successive program loops.
 Additional activities include the creation of a two-player rapid-clicker game,
 simulating a real-world toggle button, and counting switch contact bounce.
==============================================================================*/

#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

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

// Program constant definitions
const unsigned char maxCount = 50;

// Program variable definitions
unsigned char SW2Count = 0;
bool SW2Pressed = false;

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)
	{
        // Count SW2 button presses
        if(SW2 == 0)
        {
            LED3 = 1;
            SW2Count = SW2Count + 1;
        }
        else
        {
            LED3 = 0;
        }
        
        if(SW2Count >= maxCount)
        {
            LED4 = 1;
        }
        
        // Reset count and turn off LED D4
        if(SW3 == 0)
        {
            LED4 = 0;
            SW2Count = 0;
        }
        
        // Add a short delay to the main while loop.
        __delay_ms(10);
        
        // Activate bootloader if SW1 is pressed.
        if(SW1 == 0)
        {
            RESET();
        }
    }
}

Program Operation

The top of the program contains declaration statements that define the names, types, and values of global constants and variables:

// Program constant definitions
const unsigned char maxCount = 50;

// Program variable definitions
unsigned char SW2Count = 0;
bool SW2Pressed = false;

Each declaration statement begins with the numeric type declarations: const unsigned char for an 8-bit constant, unsigned char for an 8-bit variable, and bool for a single-bit variable. 

The constant or variable name follows the type. The names used here are shown in what is known as camel-case format. Camel-case names usually begin with lower case, and each successive word is joined with a capital letter, as in the constant name maxCount. The SW2Count variable keeps the capitalization of the SW2 button label. In general, it helps programmers to use descriptive names – for example, maxCount = 50 is more meaningful than m = 50.

Finally, values can be assigned to each constant or variable using an equal sign. Values have to be assigned to constants, and are optional for variables. If no value is assigned to a variable, then space for the variable is reserved in RAM but may be left uninitialized by the compiler, meaning you shouldn’t assume you know the value until after you write a value into it.

The main while( ) loop (is not what you think it is)

Following the constant and variable declarations, the main( ) function and its while( ) loop are very similar to the same sections in the Intro-1-Input-Ouput program. The big difference is the addition of multiple if-conditions within the while( ) loop, with one checking SW2 to increment the count, another checking SW3 to clear the count, and one more to check if the maximum count has been reached. There is also a delay – which doesn’t immediately seem to do anything – and the SW1 check for the bootloader at the bottom of the program.

What is this program meant to do? If you look over the code, it looks simple enough – it looks like it will count SW2 button presses and light an LED once the number of pressed exceeds a certain value.

Fair warning: this is a poorly-written program that will (probably) not work as you expected. It is meant to serve as an example of how microcontrollers expect programmers to think differently about input devices, and to help you develop an understanding of how those differences can result in some unusual behaviour. Let’s look at each part of the code to try to figure it out:

        // Count SW2 button presses
        if(SW2 == 0)
        {
            LED3 = 1;
            SW2Count = SW2Count + 1;
        }
        else
        {
            LED3 = 0;
        }

This first if-else structure lights LED3 when pushbutton SW2 is pressed (its input pin is low, or logic 0), and also increments the SW2Count variable by one each time the condition is true. If SW2 is released (its pin is high, or logic 1), LED3 is turned off. This part seems fairly straightforward.

        if(SW2Count >= maxCount)
        {
            LED4 = 1;
        }

The next if-structure compares the SW2Count variable to the maxCount constant. If SW2Count is greater or equal to maxCount, LED4 is turned on. This is a very simple structure, though it is important to realize that LED4 will stay on following this since there is no code here to turn LED4 off. But, the ability to turn LED4 off might actually be a useful feature to have, so there is this next part…

        // Reset count and turn off LED D4
        if(SW3 == 0)
        {
            LED4 = 0;
            SW2Count = 0;
        }

The third if-structure both turns off LED4 and resets the SW2Count variable to zero if SW3 is pressed. Again, nothing seemingly unusual here.

        // Add a short delay to the main while loop.
        __delay_ms(10);

Following the if-structures we find this lone 10 millisecond delay. Unlike the delays between the light patterns in the Intro-1-Input-Output program, this one just seems to waste time in the while( ) loop. And, that is exactly its purpose – to add a bit of delay and to limit the rate at which the contents of the while( ) loop run to only 100 times per second.

So, why won’t it work? Build it, download the program into the microcontroller, and see if you can figure it out.

Learn more – program analysis activities

Did you run it? This program should light LED D3 every time SW2 is pressed, and light LED D4 once the count reaches 50. Switch SW3 resets the count so you can perform repeated attempts. Try it, and count how many times you pressed SW2 before LED D4 turned on.

Did your count reach 50? Can you describe what the program is doing? (Hint: try pressing and releasing the button at different rates of speed.)

Debugging using LEDs

If you have figured out what the program is doing yet, and especially if you haven’t, we can use the LEDs to assist us in understanding and debugging the code. Modify the second 'if' structure to add the else block, shown below. This will force LED D4 off whenever SW2Count is less than maxCount.

        if(SW2Count >= maxCount)
        {

            LED4 = 1;
        }
        else
        {
            LED4 = 0;
        }

Now, press and hold pushbutton SW2 for at least 10 seconds while watching LED D4. Did it stay stay off? Did it stay on? Did it blink on and off?

LED D4 should stay off while the value of SW2Count is less than maxCount. Similarly, LED D4 should stay on while the value of SW2Count is higher than maxCount. If LED D4 is turning on and off, what can we infer about the value of the SW2Count variable? It must be going above and below maxCount! But how could this be? There is only code in program to increase the value of the SW2Count variable, and none to decrease it.

There is a limit to how high we can go

The variable SW2Count was defined as an unsigned char near the top of the program. Referring back to the numeric types chart, way back near the top of this page, we can see that unsigned characters can range between the values 0 and 255. As the variable counts up it will eventually reach 255 – and, 8-bit character variables cannot count higher than 255. After reaching 255, the next increment to that variable should bump its value to 256. Since 256 is the 9-bit binary number 100000000, and since SW2Count is stored as an 8-bit variable, only the lowest 8 bits are kept – so 256 becomes binary 00000000, or zero. This is known as an overflow, and effectively resets the count. Now that the count is zero (or less than maxCount), the else statement in the comparison will cause LED D4 to turn off.

To avoid overflowing the count, we can set a limit on the SW2Count variable by encapsulating its increment statement inside a conditional statement.

In your program, replace the single line SW2Count = SW2Count + 1; statement with the code block shown below:

            if(SW2Count < 255)
            {
                SW2Count += 1;
            }

This code demonstrates the use of an assignment operator += to shorten the previous statement SW2Count = SW2Count + 1;

The same action of adding 1 to the current SW2Count value is performed, just in a more compact form. More importantly, adding this if-condition will only allow SW2Count to be incremented up to 255. The next time through the loop the condition will be false, skipping the assignment operation, and holding the count at 255.

Current state, not change of state

With the above fix added to the program, pressing and holding pushbutton SW2 will light LED D4 after maxCount has been reached, and will keep LED D4 on because SW2Count is prevented from resetting to zero. But, holding the button down still allows the count to increase. You can try it by resetting the count using SW3, and pressing and holding button SW2. LED D4 will still turn on. Let’s explore why.

The key to understanding this program (and most microcontroller programs) is that the program is not event-based. That means that the program does not wait for pushbutton SW2 to be pressed, but rather senses its state through each cycle of the main while( ) loop. If the pushbutton is pressed when the loop gets to that section, another count is added to the SW2Count variable. If this happens every time through the loop, SW2Count will exceed maxCount simply by holding the button for the duration of the number of loops required. Running at a hundred loops per second – due to the 10ms delay in each loop – it will only take half of a second to exceed a maxCount of 50.

Rather than responding to state, the program needs to be made to respond only to each new press – in other words, a change of SW2 state. We can do this by monitoring the change of SW2 from not-pressed to pressed. Doing this requires the use of another variable to store the prior state of SW2, so that its current state can be evaluated as being the same, or different, from its state in the previous loop. Since this variable needs to only store one of two values, a Boolean variable can be used. The Boolean variable SW2Pressed has been defined exactly for this purpose at the top of the program.

Replace the initial SW2 if-else condition in your program with the following two if conditions:

        // Count new SW2 button presses
        if(SW2 == 0 && SW2Pressed == false)
        {
            LED3 = 1;
            if(SW2Count < 255)
            {
                SW2Count = SW2Count + 1;
            }
            SW2Pressed = true;
        }
        // Clear pressed state if released
        if(SW2 == 1)
        {
            LED3 = 0;
            SW2Pressed = false;
        }

These two if conditions use the Boolean SW2Pressed variable to remember the state of SW2 for the next cycle of the main while loop. Boolean variables can store the values 0 or 1, which our compiler can interchangeably represent as false or true, respectively.

The first if condition compares the current SW2 state with the previously stored SW2Pressed variable, and only allows a new count be added when button SW2 is pressed and the Boolean SW2Pressed variable is false. If SW2 was pressed, the SW2Pressed variable is set to true at the end of the first if-structure. Doing this will prevent the contents of this same if-structure from running the next time through the program loop, since the AND condition of the if-statement requires SW2Pressed to be false.

The second if structure, above, resets SW2Pressed to false whenever the button is released, allowing the next new press of SW2 to enter the first if-structure again.

Try the code to make sure it works as expected. Pressing and holding SW2 should no longer increase the count, and you will have to repeatedly press and release SW2 to turn LED D4 on.

Logical NOT

The conditional statement in the first if condition can also be written with the logical NOT operator – typed as ! (an exclamation mark):

        if(SW2 == 0 && !SW2Pressed)

Read the !SW2Pressed expression as 'not SW2Pressed' – it is the equivalent of variable SW2Pressed being false. Conversely, if we used the SW2Pressed variable name by itself in a condition – without an exclamation mark – it would be the equivalent of being true.

Defined states

Any variable’s state can be defined as a word or constant to make the code more readable. Add the following definitions to the Program constant definitions section at the top of the code:

#define pressed 0
#define notPressed 1

Now, instead of comparing the state of the button to 0 or 1, the button input can be compared to the named definitions representing 0 or 1, which makes the program more readable, though at the expense of hiding the actual switch value in the definition statement. Try it in your code, and modify the SW3 reset button to work the same way.

        // Count new SW2 button presses
        if(SW2 == pressed && SW2Pressed == false)
        {
            LED3 = 1;
            if(SW2Count < 255)
            {
                SW2Count = SW2Count + 1;
            }
            SW2Pressed = true;
        }
        // Clear pressed state if released
        if(SW2 == notPressed)
        {
            LED3 = 0;
            SW2Pressed = false;
        }

Activity 2 learning summary

Program variables can represent different sizes and types of numbers. It is an important programming requirement to specify the type of each variable, and to select the best numeric type to represent each variable’s range. In a small, relatively slow microcontroller like the PIC16F1459, selecting the smallest variable types can both reduce memory requirements and improve processor performance.

Since most microcontroller programs run their code as part of a main loop, statements that check the state of inputs run repeatedly and should not be used to count individual input events. To detect a change in input, the state of the input has to be remembered from loop to loop, and counted only when the state changes.

Binary numbers can overflow and underflow their bit sizes when incremented or decremented, creating unexpected results. Implement bounds checking to prevent numeric variables from going out of range.

While the I/O circuitry of a board such as the UBMP4 is very limited, LEDs can be used for debugging purposes using code that turns them on or off when specific values have been reached, or when certain parts of a program are run.

Programming Activity 2 Challenges

1. Can you make a two-player rapid-clicker style game using this program as a starting point? Let's use SW5 for the second player's pushbutton so that the two players can face each other from across the UBMP4 circuit board. Duplicate SW2Count and SW2Pressed to create SW5Count and SW5Pressed variables. Then, duplicate the required if condition structures and modify the variable names to represent the second player. LED D4 can still light if player 1 is the first to reach maxCount. Use LED D5 to show that the second palyer wins. Use a logical condition statement to reset the game by clearing the count and turning off the LEDs if either SW3 or SW4 is pressed.

2. Use your knowledge of Boolean variables and logical conditions to simulate a toggle button. Each new press of the toggle button will 'toggle' an LED to its opposite state. (Toggle buttons are commonly used as push-on, push-off power buttons in digital devices.)

3. Do your pushbuttons bounce? Switch bounce is the term that describes switch contacts repeatedly closing and opening before settling in their final (usually closed) state. Switch bounce in your room light switch is not a big concern, but switch bounce can be an issue in a toggle button because the speed of a microcontroller lets it see each bounce as a new, separate event. Use a variable to count the number of times a pushbutton is pressed and display the count on the LEDs. Use a separate pushbutton to reset the count and turn off the LEDs so that the test can be repeated. To determine if your switches bounce, try pressing them at various speeds and using different amounts of force.

4. A multi-function button can be used to enable one action when pressed, and a second or alternate action when held. A variable that counts loop cycles can be used to determine how long a button is held (just like the first program did unitentionally, by its design, before it was modified). Make a multifunction button that lights one LED when a button is pressed, and lights a second LED after the button is held for more that one second. (Hint: how long is the delay in each loop, and how many loops need to run to get to one second?)

5. Did your pushbuttons bounce? Think of a technique similar to the multi-function button that could be implemented to ignore switch bounce in your program.