5
CONTROLLING THE OUTPUT AND READING THE INPUT

General

After all is said and done, it’s about the input and output, and what happens between the two of them. It has to do with how we use the many capabilities provided within each PIC/MCU, and how creative we are about using those capabilities. In this chapter, we start learning to use all the input and output capabilities provided on the 16F877A using LAB-X1.

All the programs we will be discussing are provided on the support Web site (www.encodergeek.com), which is maintained as part of this book. Simply navigate to your area of interest on the Web site.

You can copy the files from the Web site and run them on the 16F877A in the LAB-X1. The exercises listed in each chapter are exercises that are designed to increase your familiarity and competence with the 16F877A. The answers to them are not provided.

In preparation for writing programs, set up the LAB-X1 so it can be programmed with one mouse button click or by pressing F10, as described in detail in Appendix A of this book.

The I/O that uses ICs in the seven empty sockets on the LAB-X1 board is covered separately in Chapters 7 and 8. These chapters also cover one-wire memory, A-to-D converters, and a number of related thermometric devices.

The I/O that uses the serial port (as RS-232 or RS485) is covered in Chapter 8. This covers communications between PICs and personal computers.

We will learn about input and output by first writing simple programs that control the outputs, and then write programs that read the inputs. We will learn how to control the outputs first, because this can be done directly from the software without need for any input. Once we control the output, we will learn how to read the inputs and make the outputs respond to them in a useful and coherent manner. The following is a list of the programs to be developed.

Programs That Create Output

The output is what we are looking for in any instrument. It is the final result of all the work we do. We will interact with all of the output devices on the LAB-X1 in these programs.

BASIC PROGRAMS

sqr Write a program to control one LED on the bargraph.

sqr Control all eight LEDs in the bargraph consecutively.

sqr Dim and brighten one LED (creating pulses of various lengths).

sqr Write “Hello World” to the LCD on its two lines.

sqr Write binary and decimal values to the LCD.

sqr Output a simple tone on the speaker.

sqr Output a telephone tone signal on the speaker.

ADVANCED PROGRAM

sqr Move an R/C servo back and forth

Programs That Read the Inputs and Then Provide Output

1. Write a simple program to read the first column, first row button, and turn ON one LED while the button is down.

2. Read the entire keyboard and display the binary value of the row and column read on the LCD.

3. Read the keyboard and display decimal key number on the LCD.

4. Read one potentiometer and display its 8-bit value on the LCD in binary, hex, and decimal notation. Also display the binary value on the bargraph.

5. Read all three potentiometers and display their values on the LCD.

6. Advanced: Use the three potentiometers to control an R/C servo. Control the location of the center position, the limit position of the end positions, and the rate of movement. Use three switches on the keyboard to move the servo clockwise, center the servo, and move it counterclockwise.

Creating Outputs

It will be easier if we learn to control the outputs first because we can do this with programs that we write without the need for any additional hardware or input signal. We will start with the simple control of LEDs, proceed to the control of the two-line LCD, which is provided on the LAB-X1, and then move on to using the speaker and an R/C hobby servo.

Let’s start with the standard “turning an LED ON and OFF” program. We will use one of the LEDs in the ten LED bargraph that is provided on the LAB-X1. We have control of only the eight rightmost LEDs on this bargraph. The leftmost LED is the power ON indicator and the one next to it comes ON if we were using a common cathode arrangement for the bargraph (as opposed to the common anode arrangement as it is currently configured).

The circuitry we are interested in is shown in Figure 5.1. All other circuitry of the LAB-X1 is still in place, but we have suppressed it so we are not distracted by it and can concentrate on the LED that is of interest to us. (PORTD.0). PORTD.0 refers to bit 0 of PORTD.

In our first exercise, we want to control the rightmost LED of the LED array. This is connected to bit 0 of PORTD in the circuitry shown in Figure 5.1. Our program needs to turn this LED ON and OFF to demonstrate that we have complete control of these two functions.

f0049-01

Figure 5.1 The LED bargraph circuitry for PORTD pin 0.

In general, the ports on the microcontrollers (MCUs) are designed so they can be used as inputs or as outputs. In fact, the ports can be programmed so certain pins on a port are inputs and others are outputs. All we have to do is tell the program what we want done and the compiler will handle the details. The compiler not only allows you to define how you will use the pins of each port, it can also set them up as inputs or as outputs automatically, depending on the instructions you use to address the pin in your program. You have a choice of setting PORTD to an output port and then setting pin 1 on this port high, or you can simply tell the compiler to make pin 1 of PORTD high and it will take care of the setting-up details.

The ports can be treated just like any other memory location in the microcontroller. By name, you can read them, set them, and use them in calculations and manipulations just like you can with any other named or unnamed memory location. If things are connected to the ports and pins, the program will interact with, and respond to, whatever is connected to them. (Any named port, register, or pin can be addressed directly by name for all purposes when using the PBP Compiler. They are called out as they are named in the datasheet for each individual PIC by the manufacturer.)

Blink One LED

Type the following program (Program 5.1) into your PC and save it. It does not need to be saved in the same directory as the PBP.exe program. To keep with the conventions being used in the compiler manual, call this program myBLINK.BAS so it does not overwrite the BLINK.BAS program provided on the disk that came with the LAB-X1.

The program to control pin 0 of PORTD is as follows:

Program 5.1 Controlling (blinking) an LED (Blinking an LED [We are using the rightmost LED on the bargraph])

 CLEAR          ; clear memory locations
 DEFINE OSC 4   ; osc speed
 
 LOOP:          ; main loop
   HIGH PORTD.0 ; turns LED connected to D0 ON
   PAUSE 500    ; delay 0.5 seconds
   LOW PORTD.0  ; turns LED connected to D0 OFF
   PAUSE 500    ; delay 0.5 seconds
 GOTO LOOP      ; go back to Loop and repeat operation
 END            ; all programs must end with END

The program demonstrates the most elementary control we have over an output: that of turning it ON and OFF. In this program, we did not have to set the port directions (with the TRIS command), because the HIGH and LOW command take care of that automatically. (If we used PORTD.0=1 instead of HIGH PORTD.0, we would have had to set TRISD to %11111110 first to set all lines to inputs except D0, which is shown as the only pin set to be an output.)

We will use binary notation (%11110000) for setting all pins, ports, port directions, and register values throughout this book, though you can use hexadecimal ($F0) and decimal (DEC 240) notation interchangeably. Using binary notation lets you see what each pin is doing without having to make any conversions.

Blink Eight LEDs in Sequence

In Program 5.2, we blink the eight rightmost LEDs on the bargraph, one LED at a time. The circuitry we are using for this program is shown in Figure 5.2. We do this by setting PORTD to 1 and then multiplying it by two a total of eight times to move the lighted LED left one position in each iteration. Note that the last multiplication overflows the 8-bit counter and turns all the LEDs OFF.

f0051-01

Figure 5.2 The LED bargraph circuitry to all of PORTD.

Program 5.2 Blinking eight LEDs one after the other on a bargraph

 CLEAR                  ; clear memory
 DEFINE OSC 4           ; Osc speed
 LED_ID VAR BYTE        ; call out the two variables
 A VAR BYTE             ; as 8 bit bytes
 TRISD =%00000000       ; set PORTD to all outputs
                                         ;
 MAINLOOP:              ; this loop is executed forever
   A=1                  ; initialize the counter to 1
    FOR LED_ID = 1 TO 8 ; do it for the 8 LEDs
      PORTD=A           ; puts number in PORTD
      PAUSE 100         ; pause so you can see the display
      A=A * 2           ; multiply by 2 moves lit LED left 1
                                         ; position
    NEXT LED_ID         ; go up and increment counter
 GOTO MAINLOOP          ; do it all forever
 END                    ; always end with END

Dim and Brighten One LED

In Program 5.3, we demonstrate the ability to dim an LED (at PORTD.0) by varying the duty cycle of the ON signal to the LED.

Program 5.3 Turns on an LED and dims the one next to it (Doing it this way lets you compare the brightness of the two LEDs)

 CLEAR                     ; always start with a CLEAR statement
 DEFINE OSC 4              ; osc speed
 TRISD = %11111100         ; set only PORTD pin 0 and 1 to outputs
 X VAR BYTE                ; declare X as a variable
 PORTD.1=1                 ; turned PORTD.1 ON & compare to PORTD.0 ;

 LOOP:                     ; start of loop
   FOR X = 1 TO 255 STEP 2 ; set up loop for X
     PWM PORTD.0, X, 3     ; vary the duty cycle
     PAUSE 200/X           ; pauses longer for the dimmer values.
   NEXT X                  ; end of loop for X
 GOTO LOOP                 ; return and do it again
 END                       ; all programs with an END statement

With the preceding programs, we learned that we can control the ON-OFF state and the brightness on an LED. Controlling the brightness will become relevant when we are controlling seven segment displays because the LEDs in them are turned ON one at a time and the duty cycle has to be managed properly to get an acceptable display.

LCD Display

These notes describe the use of, and interactions with, existing hardware connections to the liquid crystal display as it comes wired on the LAB-X1 module. These connections are defined in the program with DEFINE statements. Define them as necessary in your designs, keeping in mind that designs that follow the connections for LAB-X1 will allow you to use the LAB-X1 as the test platform. (Even if your design is substantially different from the LAB-X1 circuitry, this will remain a useful feature for testing the LCD and certain I/O that may be the same.)

On the LAB-X1, the LCD data is fed from PORTD, and all 8 bits of this port are connected to the LCD. It is controlled from the 3 bits of PORTD. You therefore have the choice of using only the 4 high bits or PORTD as a 4-bit data path for the LCD or using all 8 bits. The entire port is also connected to eight of the LEDs on the ten-light LED bargraph. (The two leftmost LEDs in the bargraph are used to indicate that the power to the LAB-X1 is ON and the polarity of the bargraph.) The 4 high bits, bits D4 to D7, cannot be used for any other purpose if the LCD is being used. The software does not release these 4 bits automatically after using them to transfer information to the LCD but you do have the option of saving the value of PORTD before using the LCD, and then restoring this value after the LCD has been written to. The complication, of course, being that there will be a short glitch when the LCD is written to and the use you make of PORTD has to tolerate this discontinuity.

PORTE, which has only three external lines, is dedicated to controlling the information transfer to the LCD. These lines can be used for other purposes if the LCD is not being used. PORTE is made digital when controlling the LCD and can be used for analog inputs when its pins are specified as analog inputs. This is done with the ADCON1 register, as described earlier and in Chapter 9.

The LCD provided on the LAB-X1 allows us to display two lines of 20 characters each. Its connections to the microcontroller are shown in Figure 5.3.

Since this is important, let’s take another look. In Figure 5.3, we see that the LCD uses all the lines available on PORTD and PORTE. All of PORTD is used as the port the data will be put on, while PORTE, which has only three lines, is used to control data transfer to the LCD. We also know from looking at the full schematics provided with the LAB-X1 that all of PORTD is also connected to the LED bargraph. This does not affect our programming of the LCD and we will ignore this for now. You will, however, see the LEDs in the bargraph flicker ON and OFF as programs run, because we will be manipulating the data on the lines (D0 to D7). It is also possible to feed the LCD with just the 4 high bits of PORTD. See the PBP manual on how to do this. It takes slightly longer to refresh the LCD when you are using only 4 bits, and writing to the LCD is one of the most time-consuming tasks in most programs.

Let’s write the ubiquitous “Hello World” program for the LCD as our first exercise in programming the LCD (see Program 5.4). Once we know how to do that, we can basically write whatever we want, when we want, to the LCD display.

f0054-01

Figure 5.3 The LCD display wiring. (An easy-to-comprehend schematic diagram showing just the lines between the microcontroller and the 2 × 20 display module.)

Program 5.4 Displaying and blinking “HELLO WORLD” in the LCD display

For the LCD display registers on the LAB-X1, the DEFINE statements are as follows:

 CLEAR                     ; clear the memory
 DEFINE OSC 4              ; osc speed           ]
 DEFINE LCD_DREG PORTD     ; data register
]
 DEFINE LCD_RSREG PORTE    ; select register     ]
 DEFINE LCD_RSBIT 0        ; select bit          ] These DEFINEs
 DEFINE LCD_EREG PORTE     ; enable register     ] are all explained
 DEFINE LCD_EBIT 1         ; enable bit          ] in the PBP
 DEFINE LCD_RWREG PORTE    ; read/write register ] manual.
 DEFINE LCD_RWBIT 2        ; read/write bit      ]
 DEFINE LCD_BITS 8         ; width of data path  ] Can be 4 or 8
 DEFINE LCD_LINES 2        ; lines in display    ]
 DEFINE LCD_COMMANDUS 2000 ; delay in micro seconds ]
 DEFINE LCD_DATAUS 50      ; delay in micro seconds ]
                                            ;
 ; Set the port directions. We are setting (must set) all of PORTD
 ; and all of PORTE as outputs even though PORTE has only three
 ; lines. (The low nibble in PORTD can be set as inputs if we use
 ; a 4 high bit path to feed the LCD.)
                                               ;
 PAUSE 500                    ; allow for LCD startup
 TRISD = %00000000            ; set all PORTD lines to output
 TRISE = %00000000            ; set all PORTE lines to output
                                               ; set the Analog-to-Digital control register
 ADCON1=%00000111             ; needed for the 16F877A - see above and
                              ; below - this makes all of ports A and E
                              ; digital.
 LOOP:                        ; the main loop of the program
   LCDOUT $FE, 1              ; clear screen, go to position 1
   PAUSE 250                  ; pause 0.25 seconds
   LCDOUT “HELLO”             ; print
   LCDOUT $FE, $C0            ; go to second line, first position
   LCDOUT “WORLD”             ; print
   PAUSE 250                  ; pause 0.25 seconds
 GOTO LOOP                    ; repeat
 END                          ; all programs must end in END

Before we can write to the LCD, we have to define how the LCD is connected to the MCU. Also, since the 16F877A has some analog capabilities, it will start up and reset in analog mode, and has to be changed to digital mode (for PORTE only in our immediate case) before we can use its digital properties.

The compiler manual tells us we have to specify the location of the LCD and specify the control lines connected to it so the compiler can address the device properly. Doing so lets us place the LCD wherever it’s convenient for us when we design our own devices (and the compiler will still be able find it). These variables are to be specified in DEFINE statements before any of the rest of the program is written. (In this book, we will always place the LCD at the same address locations used by the LAB-X1 so we can test all our programs for our instruments on the LAB-X1 when we need to.)

Program 5.4 demonstrates the most elementary control over output to the LCD display. Variations of these lines of code, and the addition of a few command codes, will be used to write to the LCD in all our programs.

Not all the preceding DEFINE statements are needed on the LAB-X1, but when you build your own devices, you will need to include them all to make sure nothing has been omitted. You never know what state an MCU might start up in, so cover all your bases.

Controlling the Digital and Analog Settings

ADCON1 = Analog to Digital CONtrol register #1.

The ADCON1=%00000111 statement, or one like it, is needed for the 16F877A because any PIC MCU processor that has any analog capabilities at all comes up in the analog mode on reset and startup. This particular instruction puts all the analog pins on ports A and E into the digital mode. Since we need only PORTE and PORTD for controlling the LCD, none of PORTA needs to be in digital mode. I am showing %00000111 because all the examples used by microEngineering Labs in all their literature and on their Web site use and suggest using this value. See datasheet for more detailed information. (The use of this register is also discussed in greater detail in Chapter 9.) If you want to turn just the three available lines on PORTE to digital, you can use any binary value from 010 to 111 inclusive.

The control of the A-to-D conversion capability is managed by the 4 low bits of ADCON1. For our purposes, bit 0 and bit 3 are not relevant. See page 112 of the datasheet for detailed information on this.

Bit 0 is not relevant to the LCD operation (it is a “don’t care” bit).

Bit 1 and 2 must be set to 1 to make the two ports (A and E) digital.

Bit 3 is not relevant to the LCD operation (it, too, is a “don’t care” bit).

So ADCON1 = %00000110 or %00000111 would be adequate for our work. (We could also have done this in decimal format with ADCON1=6 or with ADCON1=7.)

Writing Binary, Hex, and Decimal Values to the LCD

The value of numbers written to the LCD can be specified with prefixes that determine if the value will be display as a binary, a hexadecimal, or a decimal value. See the PBP manual for details.

BIN specifies that the display will be binary.

HEX specifies that the display will be in hexadecimal format.

DEC specifies that the display will be in decimal format.

In Program 5.5, the value of “NMBR” is set to 170 arbitrarily (actually because it alternates 1s and 0s in binary format). Any number below or equal to 255 could have been used. Using BIN8 instead of BIN displays all 8 bits. Using Hex2 instead of HEX displays both hex digits. DEC5 can display all five decimal digits because we are limited to 16 bits (65,535) and integer math in PICBASIC. BIN16 can be used for 2-byte words to display all 16 bits. Any number of digits up to 16 can be displayed.

Program 5.5 Writing to the LCD display in FULL binary, hexadecimal, and decimal

 CLEAR                  ; clear memory
 DEFINE OSC 4           ; osc speed
 DEFINE LCD_DREG PORTD  ; define LCD connections
 DEFINE LCD_DBIT 4      ; define LCD connections
 DEFINE LCD_RSREG PORTE ; define LCD connections
 DEFINE LCD_RSBIT 0     ; define LCD connections
 DEFINE LCD_EREG PORTE  ; define LCD connections
 DEFINE LCD_EBIT 1      ; define LCD connections
 ADCON1=%00000110       ; make PORTA and PORTE digital
 LOW PORTE.2            ; LCD R/W low (write) We will do no reading
 PAUSE 500              ; wait for LCD to start up ;

 NMBR VAR BYTE          ; assign variable ;

 TRISD = %00000000      ; D7- -D0 = all outputs
 NMBR = %10101010       ; this is decimal 170 ;

 LCDOUT $FE, 1          ; clear the LCD
 LCDOUT $FE, $80, BIN8 NMBR,” “,HEX2 NMBR, “ ”, DEC5 NMBR,” ”
                        ; display
 END                    ; end program

Reading a Potentiometer and Displaying the Results on the LED Bargraph

NOTES ON READING THE POTENTIOMETERS ON THE LAB-X1

Each potentiometer is placed across ground and 5 volts and the wiper is read on the A/D line. (Other reference voltages and resistances can be specified and used—see the datasheet.) The potentiometer value has to be high enough so as not to act as a short between the power and ground. 5K ohms as a minimal value is okay. (Extremely high values make for a jittery reading.)

When we read a potentiometer, the MCU divides the voltage across the potentiometer into 256 steps between 0 and 255 and gives us the number that represents the position of the wiper across the connected voltage. Neither the voltage nor the resistance of the potentiometer is relevant (though it can be if we know the minimum and maximum voltage across the pot). What we are getting is the relative position of the wiper expressed as an 8-bit number. (The PIC also has a 10-bit resolution capability—see the datasheet and PICBASIC manual.)

The overall resistance of each of the three potentiometers is placed across 5 volts, and the three PORTA lines read the position of the three wipers. The potentiometers are read as 8-bit values using an 8-bit A-to-D converter. This gives a full-scale reading of from 0 to 255 for all three potentiometers, no matter what the actual total resistance value of each potentiometer. If you want to read the resistance in ohms, you must divide the reading by 255 and multiply by the total resistance of the potentiometer. (Again, the potentiometer value must be high enough so the potentiometer does not act as a short between ground and the MCU power connection. 5K to 10K ohms is a good selection for most purposes.)

Next, we will read just one of the potentiometers (the one nearest the edge of the board) to an accuracy of 8 bits and display the results on the rightmost eight LEDs of the LED bargraph. This potentiometer is connected to pin 2 of the PIC (also identified as RA0 and as pin PORTA.0). We will display the result of the value read (0 to 255) on the bargraph by loading the reading into PORTD. Since PORTD is connected to the eight LEDs, this will automatically give us a binary representation of the data. In the next step, we will display the information on the LCD display as alphanumeric data (which is, of course, much easier to read).

Expanding the program to not only display to the bargraph but also put the information on the LCD display, we have a problem in that the PORTD lines are shared by the bargraph and the LCD display. When we run the program, we will notice that there is a background noisy blinking of the LEDs in the bargraph as the LCD is written to, but after that is done the bargraph displays the data from the potentiometer as expected. If we had hardware and software that could suppress the LEDs when we were writing to the LCD, this problem could be eliminated. The operation observed demonstrates that the chip select line allows us to use the lines of PORTD to control both the LED bargraph and the LCD display. Once we are done with writing to the LCD we can load the data into PORTD and pause the program to allow us to read the display. Notice that the pause/delay must come immediately after setting PORTD to A2D_Value for this to work properly. When we do what, is important when using microcontrollers. Program 5.6 shows us how this is done and Figure 5.4 shows the relevant circuitry.

Program 5.6 Displaying the potentiometer wiper position on the LCD and the LED bargraph

 CLEAR                  ; define LCD connections
 DEFINE OSC 4           ; osc speed
 DEFINE LCD_DREG PORTD  ; define LCD connections
 DEFINE LCD_DBIT 4      ; define LCD connections
 DEFINE LCD_RSREG PORTE ; define LCD connections
 DEFINE LCD_RSBIT 0     ; define LCD connections
 DEFINE LCD_EREG PORTE  ; define LCD connections
 DEFINE LCD_EBIT 1      ; define LCD connections
 ADCON1=%00000110       ; Make PORTA and PORTE digital
 LOW PORTE.2            ; LCD R/W low (set it to write only)
 PAUSE 500              ; wait for LCD to start up
                        ;
 NUMB VAR BYTE          ; assign variable
                        ;
 TRISD = %00000000      ; D7 to D0 are all made outputs
 A2D_VALUE VAR BYTE     ; create A2D_Value to store result
 TRISA = %11111111      ; set PORTA to all input
 ADCON1 = %00000010     ; set PORTA analog input
                        ;
 LCDOUT $FE, 1          ; clear the LCD
                        ; define ADCIN parameters
 DEFINE ADC_BITS 8      ; set number of bits in result
 DEFINE ADC_CLOCK 3     ; set clock source (3=rc)
 DEFINE ADC_SAMPLEUS 50 ; set sampling time in uS ;

 LOOP:                  ; start loop
   ADCIN 0, A2D_VALUE     ; read channel 0 to A2D_Value
   LCDOUT $FE, $80, “VALUE= ”, HEX2 A2D_VALUE, “ ”, DEC5_A2D_VALUE
   LCDOUT $FE, $C0, BIN8 A2D_VALUE     ;
   PORTD=A2D_VALUE      ; the pause must come right after setting
                        ; PORTD and before PORTD is used again
     PAUSE 250          ; try setting PORTD before the LCDOUT
 GOTO LOOP              ; do it forever
 END                    ; end program

f0059-01

Figure 5.4 The basic circuitry for the three potentiometers. (The information read from potentiometer 0 is displayed on the bargraph in Program 5.6.)

A Simple Beep

We have one other piece of hardware we can output to on the LAB-X1 and that is the small piezoelectric speaker on the board. This speaker is connected to line PORTC.2.

The PWM (Pulse Width Modulation) command can be used to create a short beep on the piezoelectric speaker on the LAB-X1. The command specifies the PORTC pin to be used, the duty cycle and the duration of the beep (100 milliseconds in our case). See Program 5.7.

Program 5.7 Generates a short tone on the piezo speaker (Note that line C2 is HPWM Channel 1)

 CLEAR                 ; clear memory
 DEFINE OSC 4          ; osc speed
 PWM PORTC.2, 127, 100 ; beep command
 END                   ; end the program

Program 5.7 provides a 0.1 second (100 μsec) beep. Circuitry is shown in Figure 5.5. Press the reset button to repeat the beep.

Check to see what happens if you leave the END statement off.

Program 5.7 generates a 50 percent duty cycle for 100 cycles.

f0060-01

Figure 5.5 The basic circuitry for generating tones on the piezo speaker on the hardware provided. (If you use the infrared receiver, its signal will appear on line A4. Only relevant circuitry is shown.)

It defines that we are using a 4 MHz oscillator.

PORTC.2 specifies the pin to be used.

127 specifies a 50 percent duty cycle; the range of the variable is from 0 to 255.

100 specifies that the tone is to last for 100 of the 256 ON-OFF steps that define one cycle.

In the PWM command, the frequency and length of the signal generated are dependent on the oscillator frequency. In our case, this is 4 MHz, and one cycle is about 2.5 milliseconds long (0.0025 seconds).

Note that the line C2 is also connected to the output for a possible phone jack and to an IR LED that can be used to interact with IR receivers. These two connections are not populated on the PC board as received but they can be added without difficulty.

Two types of signals can be annunciated on the speaker as programmed from the compiler. The PWM command can send a signal of a fixed duty cycle for a fixed number of cycles, and the HPWM command can set up a PWM signal that runs continuously in the background. In either case, the signal needs to be provided on the PORTC.2 pin because that is where the speaker is connected. However, the normal PWM (not the background HPWM) command signal can be made to appear at any available pin. The background HPWM signal can be modified “on the fly” in a program.

The HPWM signals can only be made available at pin PORTC.2 (Channel 1) and PORTC.1 (Channel 2). Yes, the pin numbers are reversed! In the PIC 16F877A, there are only two HPWM channels and these two pins are connected permanently to these two channels. (Some PIC devices provide more than two channels. See the datasheets.) Since we have the speaker hardwired to PORTC.2, we can only use HPWM Channel 1 for the tones we generate.

As seen in Figure 5.5, these signals can also be used to generate telephone dial tones (DTMF) and infrared (IR) signals when provided with the appropriate hardware. We will concentrate on creating tones on the piezo speaker. The wiring and programming is the same for the other devices; they are all wired in parallel. Just change the parameters to use them.

Program 5.8 is a slightly more complicated program and demonstrates the use of PWM to control the brightness of one of the LEDs in the bargraph.

Program 5.8 LED dimming using the PWM command

 CLEAR                     ; clear RAM
 DEFINE OSC 4              ; osc speed
 TRISD = %11111110         ; set only PORTD pin 1 to output
 X VAR BYTE                ; declare x as a variable
                           ;
 LOOP:                     ; start loop
   FOR X = 0 TO 255 STEP 5 ; ] in this loop the value
     PWM PORTD.0, X, 3     ; ] x represents the brightness
   NEXT X                  ; ] of the LED at PORTD.0
 GOTO LOOP                 ; repeat loop
 END                       ; end program

Using the HPWM (hardware PWM) command is a bit more complicated in that we must define certain parameters before we can use the command. The necessary defines are as follows:

 DEFINE CCP1_REG PORTC     ; Port to be used by HPWM 1
 DEFINE CCP1_BIT 2         ; Pin to be used by HPWM 1
 DEFINE CCP2_REG PORTC     ; Port to be used by HPWM 2
 DEFINE CCP2_BIT 1         ; Pin to be used by HPWM 2

You also have to define which timer the signal will use so other timers can be used for other purposes while the signal is being generated. If a timer is not specified, the system defaults to Timer1, the 16-bit timer.

The command is

 HPWM Channel, DutyCycle, Frequency

The commands

 DEFINE.OSC 4              ; osc speed
 HPWM 1, 127, 1500         ; generate background PWM at 1500 cps.

creates a 50 percent duty cycle PWM signal at 1500 Hz (as affected by the definition of OSC) on PORTC.2 continuously in the background. Also see program 5.9.

The command can be updated during runtime from within a program. As might be expected, the pin cannot be used for any other purpose as long as it is generating the PWM signal. Turn OFF the PWM mode at the CCP control register to use the pin as a normal pin. See the datasheet for more information.

The frequencies generated are limited by the frequency of the oscillator being used to clock the PIC processor. The minimum frequency for the PIC 16F877A is 1221 Hz (with a 20-MHz oscillator). Not all frequencies can be generated. See the PICBASIC PRO Compiler manual for more information on other frequencies.

Program 5.9 Generates a tone on the piezo speaker (There is no looping in this program)

 CLEAR                   ; clears memory
 DEFINE OSC 4            ; osc speed
 DEFINE CCP1_REG PORTC   ; port to be used by HPWM 1
 DEFINE CCP1_BIT 2       ; pin to be used by HPWM 1
                         ; since no timer is defined,
                         ; Timer1 will be used;
 HPWM 1,127,2500         ; the tone command
 PAUSE 100               ; pause .1 sec to hear tone
 END                     ; end program to stop tone.

Next, we will generate some telephone touchtone tones on the speaker to demonstrate the capability provided by the DTMFOUT command. (See Program 5.10.)

 DTMFOUT Pin, {Onms, Offms} [Tone#{Tone#…}]

Since we will be using Pin C2, our command will look similar to the preceding because we are using default values for ONms and OFFms

Program 5.10 Generates telephone key tones on the piezo speaker (555-1212)

 CLEAR                                  ; clear memory
 DEFINE OSC 4                           ; osc speed
 DTMFOUT PORTC.2, [5, 5, 5, 1, 2, 1, 2] ; telephone tones
 END                                    ; end program

The key tones generated are rough (before filtering), but you can tell that they mimic the telephone dialing tones. The signal needs to go through a filter and then an amplifier to be clean and viable. This command has a number of constraints, depending on the processor used and the speed of the oscillator in the circuit. See the PICBASIC PRO Compiler manual for details.

The FREQOUT command can also be used to generate telephone dialing frequencies. See the PICBASIC PRO Compiler manual for details.

Advanced Exercise: Controlling an RC Servo from the Keyboard

Now that we know how to generate pulses and control the LCD, we can use the LABX1 to control the position of an RC servo connected to port J7 from switches SW1, 2, and 3 on the keyboard. Program 5.11 is designed such that …

Switch 1 will turn the servo clockwise incrementally.

Switch 2 will center the servo from wherever it is.

Switch 3 will turn the servo counterclockwise incrementally.

The circuitry for this exercise is shown in Figure 5.6.

Note that by changing a few variables that are defined up-front, we can adjust the center position, the incremental step value, and the extreme CW and CCW positions of the servo. (This program has been adapted from, and made simpler than, a program in the microEngineering Labs sample programs. It is instructive to compare this program with the programs SERVOX and SERVO1 in the sample programs.)

Program 5.11 Servo Position Control for an R/C servo from PORTB buttons (This program uses a servo on Jumper J7)

 CLEAR                                  ; clear memory
 DEFINE OSC 4                           ; osc speed
 DEFINE LCD_DREG PORTD                  ; define LCD connections
 DEFINE LCD_DBIT 4                      ; define LCD connections
 DEFINE LCD_RSREG PORTE                 ; define LCD connections
 DEFINE LCD_RSBIT 0                     ; define LCD connections
 DEFINE LCD_EREG PORTE                  ; define LCD connections
 DEFINE LCD_EBIT 1                      ; define LCD connections
 POS VAR WORD                           ; servo position variable
 CENTERPOS VAR WORD              ; servo position variable
 MAXPOS.VAR WORD                 ; servo position variable
 MINPOS VAR WORD                 ; servo position variable
 POSSTEP VAR BYTE                ; servo position step variable
 SERVO1 VAR PORTC.1              ; alias servo pin Use J7 for servo
 POS=0                           ; set variables
 CENTERPOS =1540                 ; set variables
 MAXPOS =2340                    ; set variables
 MINPOS =740                     ; set variables
 POSSTEP =5                      ; set variables
 ADCON1 = %00000111              ; PORTA and PORTE to digital
 LOW PORTE.2                     ; LCD R/W low = write
 PAUSE 100                       ; wait for LCD to startup
 OPTION_REG = $01111111          ; enable PORTB pullups
 LOW SERVO1                      ; servo output low
 GOSUB CENTER                    ; center servo
 LCDOUT $FE, 1                   ; clears screen only
                                 ;
 MAINLOOP:                       ; main program loop
   PORTB = 0                     ; PORTB lines low to read buttons
   TRISB = $11111110             ; enable first row of buttons on kybd
   IF PORTB.4 = 0 THEN GOSUB LEFT; check if any button is pressed
   IF PORTB.5 = 0 THEN GOSUB CENTER    ; and make a move
   IF PORTB.6 = 0 THEN GOSUB RIGHT     ; accordingly
   LCDOUT $FE, $80, “POSITION = ”, DEC4 POS , “ ”   ;
   SERVO1 = 1                   ; start servo pulse
   PAUSEUS POS                  ;
   SERVO1 = 0                   ; end servo pulse
   PAUSE 16                     ; servo update rate about 60 Hz
 GOTO MAINLOOP                  ; do it all forever
                                ;
 LEFT:                          ; move servo left
   IF POS < MAXPOS THEN POS = POS + POSSTEP  ;
 RETURN                         ;
                                ;
 
 RIGHT:                         ; move servo right
   IF POS > MINPOS THEN POS = POS – POSSTEP  ;
 RETURN                         ;
                                ;
 CENTER:                        ; center servo
 POS = CENTERPOS                ;
 RETURN                         ;
 END                            ; end program

Now, let’s make Program 5.11 more sophisticated by using the three potentiometers on the LAB-X1 board to manipulate the three variables that control the center position, the end positions, and the incremental move of the servo in the Program 5.11. We will use just one variable to adjust both end positions because we have only three potentiometers.

f0065-01

Figure 5.6 Circuitry for controlling an RC servo from the three potentiometers. (As always, only relevant components are shown. Connect the servos to Jumper J7.)

If we had four potentiometers, we could make the adjustment to the limits on one side independent of the adjustment of the other.

We will allow the center position to be adjusted by 127 counts in each direction.

The end positions will be made variable by 127 counts at each end.

The incremental move will be adjustable from 1 to 20 counts per key press.

First, we will make it possible to read the potentiometers. We already know how to do this. Then, we will add the math relationships to the variable in the program so the readings from the potentiometers interact with the three variables appropriately. (See Program 5.12.)

Pot 0, the one nearest the board edge, controls the center position.

Pot 1, the middle pot, controls the limit positions.

Pot 2 sets the speed of the servo by setting the step amount.

Program 5.12 Use servo on jumper pins at the J7 Servo position control, with added functions

 CLEAR                        ; clear memory
 DEFINE OSC 4                 ; osc speed
 DEFINE LCD_DREG PORTD        ; define LCD connections
 DEFINE LCD_DBIT 4            ;
 DEFINE LCD_RSREG PORTE       ;
 DEFINE LCD_RSBIT 0           ;
 DEFINE LCD_EREG PORTE        ;
 DEFINE LCD_EBIT 1            ;
 DEFINE ADC_BITS 8            ; set number of bits in result
 DEFINE ADC_CLOCK 3           ; set clock source (3=rc)
 DEFINE ADC_SAMPLEUS 50       ; set sampling time in uS
 TRISA = %11111111            ; set PORTA to all input
 TRISD = %00000000            ; set all PORTD lines to outputs
 ADCON1 = %00000111           ; PORTA and PORTE to digital
 LOW PORTE.2                  ; LCD R/W line low (W)
 A2D_VALUE VAR BYTE           ; create A2D_Value to store result
 A2D_VALUE1 VAR BYTE          ; create A2D_Value to store result
 A2D_VALUE2 VAR BYTE          ; create A2D_Value to store result
 POS VAR WORD                 ; servo positions
 CENTERPOS VAR WORD           ; center position
 MAXPOS VAR WORD              ; max position
 MINPOS VAR WORD              ; min position
 POSSTEP VAR BYTE             ; step length
 PAUSE 500                    ; wait .5 second
 SERVO1 VAR PORTC.1           ; alias servo pin
 ADCIN 0, A2D_VALUE           ; read channel 0 to A2D_Value
 OPTION_REG = $7F             ; enable PORTB pull ups
 LOW SERVO1                   ; servo output low
 GOSUB CENTER                 ; center servo
 LCDOUT $FE, 1                ; clears screen only
 PORTB = 0                    ; PORTB lines low to read buttons
 TRISB = %11111110            ; enable first button row 
                              ; 

 MAINLOOP:                    ; main program loop
                              ; check any butn pres’d to move servo
   IF PORTB.4 = 0 THEN GOSUB LEFT      ; handle left move
   IF PORTB.5 = 0 THEN GOSUB CENTER    ; handle centering
   IF PORTB.6 = 0 THEN GOSUB RIGHT     ; handle right move
   ADCIN 0, A2D_VALUE            ; read channel 0 to A2D_Value
   ADCIN 1, A2D_VALUE1           ; read channel 1 to A2D_Value 1
   ADCIN 3, A2D_VALUE2           ; read channel 2 to A2D_Value 2
   MAXPOS =2350 –127 + A2D_VALUE1 ; max position relationship defined
   MINPOS =750 +127-A2D_VALUE1     ; min position relationship defined
   CENTERPOS=POS-127 + A2D_VALUE ; center position relationship defined
   SERVO1 = 1                       ; start servo pulse
     PAUSEUS POS                    ; pulse length
   SERVO1 = 0                       ; end servo pulse
   LCDOUT $FE, $80, “POS=”, DEC POS-127 + A2D_VALUE , “ “,DEC_
 A2D_VALUE,” “,DEC A2D_VALUE1,” “ ,DEC POSSTEP,” ”
 ;
   PAUSE 16                         ; servo update rate about 60 Hz
 GOTO MAINLOOP                      ; do it all forever
                                    ;
 LEFT:                              ; move servo left
   IF POS < MAXPOS THEN POS = POS + POSSTEP
 RETURN                             ;
                                    ;  
 
 RIGHT:                             ; move servo right
   IF POS > MINPOS THEN POS = POS - POSSTEP
 RETURN                             ;
                                    ; 
 CENTER:                            ; center servo
   POS = 1540-127 + A2D_VALUE       ;
 RETURN                             ;
                                    ;

 END                                ; end program

At this stage, we are starting to get an idea about how one might take a simple problem and make it amenable to a more sophisticated solution by adding simple hardware and software features to it. We have gone from a simple but rigid control of the position of a servo to a much more flexible and user-friendly approach. Adding features like these to our instruments and controllers will make them more intuitive, useful, and ergonomic.

Reading the Inputs

Now that we are beginning to learn how to control the output, we need to learn how to read the inputs and manipulate the outputs based on what the input is. In other words, we are going to learn how to create interactive, and thus maybe more useful programs, instruments, and controllers.

READ THE FIRST COLUMN, FIRST ROW PUSH BUTTON (SW1) AND TURN ON AN LED ONLY WHILE THE BUTTON (SW1) IS DOWN

The simplest input is to read just one push button and the simplest output is to turn just one LED ON. We will do just that but we will add a little complication. The LED is to be programmed to be ON only while the button is held down. We will use button 1 (top left) on the keyboard and the LED connected to PORTD.0. This emulates the operation of an ordinary momentary contact switch in any real world application.

READING THE KEYBOARD

On the LAB-X1, all of PORTB is dedicated to the interface with the keyboard. Lines B0 to B3 are connected to the rows, and the lines B4 to B7 are connected to the columns of the keyboard. When the keyboard is not being used, the lines may be used for other purposes, but keep in mind the internal pull-up capability and the in-line load limiting resistors on the lower 4 bits/lines (B0 to B3). These can, of course, easily be made to remain outside our circuitry, so none of this is a problem for us.

In other words, the keyboard is connected to PORTB such that the columns of the keyboard matrix are connected to the high nibble of a port and the rows are connected to the low nibble. The wiring schematic is shown in Figure 5.7.

To read a keyboard like this, the low nibble of PORTB is set to be outputs and the high nibble is set to be inputs.

On the PIC 16F877A, PORTB has a special property that allows its lines to be pulled high (very weakly) with internal resistors by setting OPTION_REG.7 = 0. This property of the PORT can affect all the high bits (B5 to B7) but only those bits that are actually programmed to be inputs with TRISB will be affected.

Next, the four (the low bits, B3 to B0) are made low one line at a time, while the high bits are polled to see if any of them has been pulled low. If any of the switches is down, one of the lines will be pulled low. Because we know which low bit was selected when the high bit became low, we can determine which key has been pressed. For our purposes, at this stage we are interested only in SW1, the upper left switch, so we can simplify the circuitry to what is shown in Figure 5.8 for one row, and then what is shown in Figure 5.9 for just one key.

In these diagrams, we can see that if we make PORTB.0 low and PORTB.4 has been pulled high, PORTB.4 will become low only if SW1 is held down. No polling is necessary at this stage. Once the conditions are set up, all we have to do is create a loop that turns the PORTD.0 LED ON if the switch SW1 is down and OFF for all other conditions. The code for this is listed in Program 5.13.

Program 5.13 Reading a switch (Program reads SW1 and turns LED on PORTD.0 ON while it is down)

 CLEAR               ; clear memory
 DEFINE OSC 4        ; osc speed
 TRISB = %11110000   ; set the PORTB directions
 PORTB = %11111110   ; Set only B0 made low.
                     ; See page 31 of the datasheet re:
                     ; the pull ups on PORTB
 OPTION_REG.7=0      ; bit 7 of the OPTION_REG sets the pull ups
                     ; when cleared
 TRISD = %11111110   ; set only PORTD.0 to an output.
 PORTD.0=0           ; initialize this LED to OFF                              
;

 MAINLOOP:           ;
   IF PORTB.4=1 THEN ; check for first column being low
     PORTD.0=0       ; if it is low turn D0 OFF
   ELSE              ;
     PORTD.0=1       ; if not turn it ON
   ENDIF             ;
 GOTO MAINLOOP       ; repeat.
 END                 ;

f0069-01

Figure 5.7 Keyboard wiring for the keyboard rows and columns.

f0070-01

Figure 5.8 Partial keyboard. (The wiring for just one line of switches. The other wiring is still there but is being ignored in the diagram and in the program.)

f0070-02

Figure 5.9 Just one key. (The wiring for just one switch. The other wiring is still there but is being ignored in the diagram and in the program.)

Keep in mind that in Program 5.13 we are looking at SW1 only. The other switches in this column will not turn the LED on because they are all high and cannot change the state of PORTB.4 because it is already pulled high (and needs to go LOW if we are to read it as having changed its state).

READ ENTIRE KEYBOARD AND DISPLAY THE BINARY VALUE OF THE ROW AND COLUMN READ ON THE LCD

Next, we learn how to read the entire keyboard and tell which key was pressed by identifying the active row and column numbers. This is a modification of the single key program with the scanning of the nibbles in PORTB added to determine what happened and when it happened.

A loop scans the high nibble of PORTB, the output from the keyboard. When all 4 bits are pulled high, this nibble will be read as HEX F. If it is HEX F, no keys are down and we rescan the keys. If, however, a key has been pressed, the answer will be other than HEX F and can be interpreted as follows:

If B4 is low the answer will be HEX E (15–1=14) 1110 Column 1

If B5 is low the answer will be HEX D (15–2=13) 1101 Column 2

If B6 is low the answer will be HEX B (15–4=11) 1011 Column 3

If B7 is low the answer will be HEX 7 (15–8=7) 0111 Column 4

To determine which row the key that was pressed is in, we have to know which of the bits in the low nibble had been taken LOW by the scanning routine.

The values for the low nibble are as follows:

If B0 is low the low nibble will be HEX E (15–1=14) 1110 Row 1

If B1 is low the low nibble will be HEX D (15–2=13) 1101 Row 2

If B2 is low the low nibble will be HEX B (15–4=11) 1011 Row 3

If B3 is low the low nibble will be HEX 7 (15–8=7) 0111 Row 4

Having the two pieces of preceding information lets us identify the key that was pressed. No matter how many keys we have and no matter how they are laid out, the scanning routine to read the keyboard will be something like what was just explained.

Next, in Program 5.14 we will display the contents of the entire byte on the first line of the LCD so we can actually see what is happening in the register represented by PORTB as we scan the lines. Then, on the second line we will show the low byte and the high byte separately so we can see what each key press does. We have added a 1/20 second delay in the loop (so we can see the scanned value), so we have to hold each key down for at least 1/20 second for the scan to make sure the key press will register and show in the display.

Program 5.14 Read keyboard (Reading the keyboard rows and columns)

 CLEAR                         ; clear memory
 DEFINE OSC 4                  ; osc speed
 DEFINE LCD_DREG PORTD         ; LCD defines follow
 DEFINE LCD_DBIT 4             ;
 DEFINE LCD_RSREG PORTE        ;
 DEFINE LCD_RSBIT 0            ;
 DEFINE LCD_EREG PORTE         ;
 DEFINE LCD_EBIT 1             ;
 ADCON1 = %00000111            ; make PORTA and PORTE digital
 LOW PORTE.2               ; LCD R/W low (write)
 PAUSE 500                 ; wait for LCD to start up
                           ;
 READING VAR BYTE          ; define the variables
 ALPHA VAR BYTE            ;
 BUFFER VAR BYTE           ;
                           ; set up port B pull ups
 OPTION_REG.7 = 0          ; enable PORTB pull ups to make B4-B7 high
 TRISB = %11110000         ; make B7-B4 inputs, B3-B0 outputs
 BUFFER=%11111111          ; no key has been pressed for display
                           ; set up the initial LCD readings
 LCDOUT $FE, 1             ; clear the LCD
 LCDOUT $FE, $C0, “ROW=“,BIN4 (BUFFER & $0F),” COL=”,_
 BIN4 BUFFER >>4
                           ;
 LOOP:                     ;
   PORTB =%00001110        ; set line B0 low so we can read row 1
   FOR ALPHA = 1 TO 4      ; only need to look at 4 rows
     LCDOUT $FE, $80, BIN8 PORTB,” SCANVIEW B” ; see bits scanned
     IF (PORTB & $F0)$F0 THEN    ; as soon as one of the bits in B4
                                 ; to B7 changes we immediately
                                 ; have to store the value of PORTB
       BUFFER =PORTB             ; in a safe place.
       GOSUB SHOWKEYPRESS        ;
   ELSE                          ;
   ENDIF                         ;
     PAUSE 50                    ; pause lets us see the scan but
                                 ; it also means hold a key down
                                 ; for over 50 usecs to have it
                                 ; register. Pause be removed
                                 ; after you have seen the bits
                                 ; scanning on the LCD
     PORTB= PORTB <<1    ; move bits left one place for next
                                 ; line low
     PORTB= PORTB + 1            ; put 1 back in LSBit, the right bit
   NEXT ALPHA                    ;
 GOTO LOOP                       ;
                                 ;
 SHOWKEYPRESS:                   ;
   LCDOUT $FE, $C0, “ROW=”, BIN4 (BUFFER & $0F),_
 “ COL=”, BIN4 BUFFER >>4
        ;
 RETURN                          ;
 END                             ; 

Read Keyboard and Display Key Number on the LCD

Now that we understand how this works, we have to turn the binary information we have gathered into a number from 1 to 16 and identify the key press on the LCD. (The sample program to do this, which is provided on the Internet by microEngineering Labs, shows another way of doing this and is worth studying.)

The switch number is the row number plus (the column number –1) * 4.

If we reverse all the bits in the PORTB byte, the nibbles will give us the positions of the rows and columns as the locations of the 1s in the two nibbles. Make sure you understand this before proceeding. Work it out on a piece of paper step by step.

The “Show Key Press” program must be modified to appear as shown in Program 5.15.

Program 5.15 Reading the keyboard (Reading the keyboard rows and columns and show key number)

 CLEAR                                       ; clear memory
 DEFINE OSC 4                                ; osc speed
 DEFINE LCD_DREG PORTD                       ; define LCD connections
 DEFINE LCD_DBIT 4                           ;
 DEFINE LCD_RSREG PORTE                      ;
 DEFINE LCD_RSBIT 0                          ;
 DEFINE LCD_EREG PORTE                       ;
 DEFINE LCD_EBIT 1                           ;
 ADCON1 = 7                                  ; make PORTA and PORTE digital
 LOW PORTE.2                                 ; LCD R/W low (write)
 PAUSE 200                                   ; wait for LCD to start
                                             ; define the variables
 BUFFER VAR BYTE                             ;
 ALPHA VAR BYTE                              ; counter for rows
 COLUMN VAR BYTE                             ;
 ROW VAR BYTE                                ;
 SWITCH BYTE                                 ;
                                             ; set up PORTB pullups
 OPTION_REG.7 = 0                            ; enable PORTB pullups to make B4-B7 high
 TRISB = %11110000                           ; make B7-B4 inputs, B3-B0 outputs
                                             ; set up the initial LCD readings
 LCDOUT $FE, 1                               ; clear the LCD
 LOOP:                                       ;
   PORTB =%00001110                          ; set line B0 low so we can read row 1 only
   FOR ALPHA = 1 TO 4                        ; need to look at 4 rows
     IF (PORTB & $F0)<>$F0 THEN              ;
                                             ; as soon as one of the bits in
                                             ; B4 to B7 changes we; immediately
                                             ; have to store the value of PORTB
     BUFFER =PORTB                           ;
     GOSUB SHOWKEYPRESS                      ;
     ELSE                                    ;
     ENDIF                                   ;
     PORTB= PORTB << 1                       ; move bits left one place for
                                             ; next line low
     PORTB= PORTB + 1                        ; put 1 back in LSBit, the right bit
   NEXT ALPHA                                ;
 GOTO LOOP                                   ;
                                             ;
 SHOWKEYPRESS:                               ;
   BUFFER = BUFFER ^ %11111111               ; flips all the bits in the buffer
                                             ; print the first line
   LCDOUT $FE, $80, “ROW=“,BIN4 (BUFFER & $0F),” _
 COL=”, BIN4 BUFFER >>4
                      ;
   COLUMN=(NCD BUFFER) –4                    ; calculate column
   ROW=NCD (BUFFER &$0F)                     ; calculate row
   SWITCH=((ROW-1) * 4) +COLUMN              ; calculate switch number
                                             ; print the second line
   LCDOUT $FE, $C0, “ROW=”, DEC ROW, “ COL=”, DEC COLUMN, _
 “ SW=”, DEC SWITCH, “ ”
                     ;
 RETURN                                      ;
 END                                         ;

Read One Potentiometer and Display Its 8-Bit Value on LCD in Binary, Hex, and Decimal Notation, also Impress the Binary Value on the Bargraph

A detailed discussion of A-to-D conversions is covered in the chapter, which is devoted to the construction of a digital thermometer instrument based on this capability.

As mentioned before, the potentiometers are read by dividing the voltage across a potentiometer into 256 parts and seeing which of the 256 divisions match the position of the wiper. This gives a reading between 0 and 255 (in 8-bit resolution). It does not tell us anything about the resistance of the potentiometer, only the relative position of the wiper.

We will read/use the pot closest to the edge of the board. This pot is connected to line PORTA.0, which is pin2 of the MCU.

A-to-D conversions are controlled by the ADCON0 and ADCON1 registers, and the 16F877A has to be in analog mode for the relative pins for A-to-D conversions to be possible.

Setting the bits in ADCON0. (See page 111 of the datasheet.)

Bits 7 and 6 control the clock/oscillator to be used. Set these both to 1.

Bits 5 to 3 select which channels are to be used in the conversions; set to 000 for PORTA.0.

Bit 2 cleared when the conversion is completed. Set it to 1 to start the conversion.

Bit 1 ignored in A/D conversions. Set it to 0.

Bit 0 controls A-to-D conversions. Set it to 1 to enable A-to-D conversions.

When the conversion is completed, the result will be placed in ADRESH and ADRESL. The format of how this is done depends on how the result is set up with register ADCON1.

ADCON1 needs bit 7 to be set to 0 to make the 8-bit result appear in ADRESH and bit 2 needs to be set to 1 to select potentiometer 0 and set the proper reference voltages. See page 112 of the datasheet.

So we set ADCON0 to %11000001 to set up for reading PORTA.0.

And we set ADCON1 to %00000010.

The program segment to read a value is shown in Program 5.16.

Program 5.16 Potentiometer readings (Displaying the value of the potentiometer in all formats)

 DEFINE OSC 4                     ; osc speed
 LOOP:                            ; begin loop
  ADCON0.2 = 1                    ; start conversion
  NOT_DONE:                       ; marker if not done
   PAUSE 5                        ;
  IF ADCON0.2 = 1 THEN NOT_DONE ; wait for low on bit-2 of ADCON0, conv
  A2D_VALUE = ADRESH              ; move high byte of result to A2D_Value
  LCDOUT $FE, 1                   ; clear screen

  LCDOUT “VALUE: ”, DEC A2D_VALUE,” ” ; display the decimal value
  PAUSE 100                       ; wait 0.1 second
 GOTO LOOP                        ; do it forever

The complete program would look like the following:

 DEFINE LCD_DREG PORTD              ; define LCD registers and bits
 DEFINE LCD_DBIT 4                  ;
 DEFINE LCD_RSREG PORTE             ;
 DEFINE LCD_RSBIT 0                 ;
 DEFINE LCD_EREG PORTE              ;
 DEFINE LCD_EBIT 1                  ;
 A2D_VALUE VAR BYTE                 ; create A2D_Value to store result
                                    ; set PORTA set PORTD
 TRISA = %11111111                  ; wet PORTA to all input
 TRISD = %00000000                  ; wet PORTD to all output
 ADCON0 = %11000001                 ; configure and turn on A/D Module
 ADCON1 = %00000010                 ; set PORTA analog and LEFT justify
 PAUSE 500                          ; wait 0.5 second for LCD startup
                                    ;
 LOOP:                              ;
   ADCON0.2 = 1                     ; start conversion
   NOT_DONE:                        ;
   IF ADCON0.2 = 1 THEN NOT_DONE    ; wait for low on bit-2 of ADCON0,
                                    ; conversion finishes
   A2D_VALUE = ADRESH           ; move high byte of result to A2D_Value
   LCDOUT $FE, 1                ; clear screen
   LCDOUT “DEC VALUE= ”, DEC A2D_VALUE,” ” ; Display 3 values
   LCDOUT $FE, $C0, “HEX=”, HEX2 A2D_VALUE,” ”,”BIN=”, BIN8_A2D_VALUE,” ” 
                                    ;
   PORTD=A2D_VALUE                  ; displays value in bargraph
   PAUSE 100                        ; wait 0.1 second
 GOTO LOOP                          ; do it forever
 END                                ; end program

In Program 5.16, we used the named registers themselves to set up the conversions. In the program in the next section, we will use the power of the compiler and its related commands to read the three pots much more conveniently by using the ADCIN command.

Read All Three Potentiometers and Display Their Values on the LCD

Five of the six pins on PORTA can be used as analog inputs. In our case, pins 0, 1, and 3 are connected to the three potentiometers. (Pin A4 cannot be used.)

If we want to read all three pots, we have to activate their three lines and create variables to store the three results obtained. The modifications to Program 5.16 are shown in Program 5.17.

Program 5.17 Display potentiometer settings (Reading and displaying all three potentiometers values in decimal format)

 CLEAR                      ; define LCD connections
 DEFINE OSC 4               ; osc speed
 DEFINE LCD_DREG PORTD      ;
 DEFINE LCD_DBIT 4          ;
 DEFINE LCD_RSREG PORTE     ;
 DEFINE LCD_RSBIT 0         ;
 DEFINE LCD_EREG PORTE      ;
 DEFINE LCD_EBIT 1          ;
 LOW PORTE.2                ; LCD R/W line low (W)
 PAUSE 500                  ; wait .5 second for LCD startup
                            ; the next 3 defines are needed for
                            ; the ADCIN command
 DEFINE ADC_BITS 8          ; set number of bits in result
 DEFINE ADC_CLOCK 3         ; set internal clock source (3=rc)
 DEFINE ADC_SAMPLEUS 50     ; set sampling time in uS
                            ;
 TRISA = %11111111          ; set PORTA to all input
 TRISD = %00000000          ; set all PORTD lines to outputs
 ADCON1 = %00000110         ; PORTA and PORTE to digital
 A2D_Value0 VAR BYTE        ; create A2D_Value to store result 1
 A2D_Value1 VAR BYTE        ; create A2D_Value to store result 2
 A2D_Value2 VAR BYTE        ; create A2D_Value to store result 3
                            ;
 LCDOUT $FE, 1              ; clear the display
                            ;
 MAINLOOP:                  ; main program loop
                            ; check potentiometer values
   ADCIN 0, A2D_VALUE0      ; read channel 0 to A2D_Value0
   ADCIN 1, A2D_VALUE1      ; read channel 1 to A2D_Value1
   ADCIN 3, A2D_VALUE2      ; read channel 2 to A2D_Value2
   LCDOUT $FE, $80, DEC A2D_VALUE0,” “,DEC A2D_VALUE1,” “ ,DEC_
  A2D_VALUE2,” ”
            ;
   PAUSE 10                 ;
 GOTO MAINLOOP              ; do it all forever
 END                        ; end program

Adding the Kind of Flexibility That Defines Computer Interfaces and Demonstrates the Ability to Make Sophisticated Real-Time Adjustments

Program 5.18 is similar to Program 5.16 that was developed earlier but shows another approach.

Use the three potentiometers to control one R/C servo.

Control the relative location of the center position with POT0.

Control limit position of the end positions with POT1.

Control the rate of movement with POT2.

Program 5.18 Servo/Potentiometers (Three potentiometers controlling one servo; connect the servo to Jumper J7 for this program)

 CLEAR                      ;
 DEFINE OSC 4               ; osc speed
 DEFINE LCD_DREG PORTD      ; define LCD connections
 DEFINE LCD_DBIT 4          ;
 DEFINE LCD_RSREG PORTE     ;
 DEFINE LCD_RSBIT 0         ;
 DEFINE LCD_EREG PORTE      ;
 DEFINE LCD_EBIT 1          ;
 LOW PORTE.2                ; LCD R/W line low (W)
 DEFINE ADC_BITS 8          ; set number of bits in result
 DEFINE ADC_CLOCK 3         ; set clock source (3=rc)
 DEFINE ADC_SAMPLEUS 50     ; set sampling time in uS
 TRISA = %11111111          ; set PORTA to all input
 TRISD = %00000000          ; set all PORTD lines to outputs
 ADCON1 = %00000111         ; PORTA and PORTE to digital
 A2D_VALUE VAR BYTE         ; create A2D_Value to store result
 A2D_VALUE1 VAR BYTE        ; create A2D_Value1 to store result
 A2D_VALUE2 VAR BYTE        ; create A2D_Value2 to store result
 POS VAR WORD               ; servo positions
 CENTERPOS VAR WORD         ;
 MAXPOS VAR WORD            ;
 MINPOS VAR WORD            ;
 POSSTEP VAR BYTE           ;
 PAUSE 500                  ; wait .5 second
 SERVO1 VAR PORTC.1         ; alias servo pin
 ADCIN 0, A2D_VALUE         ; read channel 0 to A2D_Value
 OPTION_REG = $01111111     ; enable PORTB pullups
 LOW SERVO1                 ; servo output low
 GOSUB CENTER               ; center servo
 LCDOUT $FE, 1              ; clears screen only
 PORTB = 0                  ; PORTB lines low to read buttons
 TRISB = %11111110          ; enable first button row
                            ; main program loop
 MAINLOOP:                  ; check any but. pressed to move servo
   IF PORTB.4 = 0 THEN GOSUB LEFT ;
   IF PORTB.5 = 0 THEN GOSUB CENTER    ;
   IF PORTB.6 = 0 THEN GOSUB RIGHT     ;
   ADCIN 0, A2D_VALUE             ; read channel 0 to A2D_Value
   ADCIN 1, A2D_VALUE1            ; read channel 1 to A2D_Value 1
   ADCIN 3, A2D_VALUE2            ; read channel 2 to A2D_Value 2
   MAXPOS =1500 + A2D_VALUE1*3    ;
   MINPOS =1500 - A2D_VALUE1*3    ;
   CENTERPOS=1500+3*(A2D_VALUE-127)    ;
   POSSTEP =A2D_VALUE2/10 +1      ;
   SERVO1 = 1                     ; start servo pulse
   PAUSEUS POS                    ;
   SERVO1 = 0                     ; end servo pulse
   LCDOUT $FE, $80, “POS=”, DEC POS , “ “’ ;
   LCDOUT $FE, $C0, DEC A2D_VALUE,” “,DEC A2D_VALUE1,” “ , DEC_POSSTEP,” ”
    PAUSE 10                      ; servo update rate about 60 Hz
 GOTO MAINLOOP                    ; do it all forever
                                  ; move servo left
 LEFT: IF POS < MAXPOS THEN POS = POS + POSSTEP
 RETURN
                           ;
                                  ; Move servo right
 RIGHT: IF POS > MINPOS THEN POS = POS - POSSTEP
 RETURN
                           ;
                                  ; center servo
 CENTER: POS = 1500+3*(A2D_VALUE-127)        ;
 RETURN                           ;
 END                              ; end program

Exercises

Caution: Thinking required!

Answers to these problems are not provided. There are no unique solutions.

Since making instruments and controllers is really all about inputs and outputs and what you do with them, a comprehensive set of exercises that focus specifically on inputs and outputs are provided.

LED EXERCISES: CONTROLLING THE LIGHT EMITTING DIODES (LEDs)

We will learn about controlling the output from a PIC by writing a series of increasingly complicated programs that will control the ten-segment LED bargraph provided on the LAB-X1. In these exercises, we are controlling the LEDs, but the control strategies developed will apply to any kind of “ON OFF” devices we have connected to the LAB-X1 or to any other device we design.

1. Light the eight LEDs on the right one at a time till they are all lit, and then turn them OFF one at a time. Time delay between actions is to be one-tenth of a second (exactly).

2. Modify the preceding program so the delay time is controlled by the top most potentiometer on the LAB-X1. The time is to vary from 10 milliseconds to 200 milliseconds inclusive, no less, no more.

3. Write a program that will vary the glow on the rightmost LED from fully OFF to fully ON once a second. Program the second LED to go dark and bright exactly 180 degrees out of phase with the first LED so that as one LED is getting brighter, the other LED is getting dimmer and vice versa.

4. Write a program that flashes the four leftmost LEDs ON and OFF every 0.25 seconds and cycles the four LEDs on the right through a bright/dim cycle every two seconds.

5. Write a program that flashes the first LED ten times a second, flashes the second one nine times a second, and flashes the third LED whenever both LEDs are on at the same time. Display how many times the third LED has blinked on the LCD display. (Timing can be approximate but has to have a common divider so the third LED will give the beat frequency.)

LIQUID CRYSTAL DISPLAY EXERCISES: CONTROLLING THE LIQUID CRYSTAL DISPLAY (LCD)

The addresses of the memory locations used by the LCD have already been fixed, as has the instruction set we use to write to the LCD. The description of the Hitachi HD44780U (LCD-II) controller instruction set as well as its electronic characteristics are provided in the data file for the display. Here, we will list only the codes that apply to our immediate use of the device.

Two types of commands can be sent to the display: the control codes and the set of actual characters to be displayed. Both uppercase and lowercase characters are supported, as are a number of special and graphic characters. The control codes allow you to control the display and set the position of the cursor, and soon. Each control code must be preceded by decimal 254 or Hex $FE. (The controller also supports the display of a set of Japanese characters that are not of interest to us.)

Command codes for the following actions are provided along with others. Go to the datasheet for the controller to find out what all these command codes are.

Clear the LCD

Return home

Go to beginning of line 1

Go to beginning of line 2

Go to a specific position on line 1

Go to a specific position on line 2

Show the cursor

Hide the cursor

Use an underline cursor

Turn on cursor blink

Move cursor right one position

Move cursor left one position

There are still other commands to discover in the datasheet. There are memory locations within the LCD, as well as invisible locations beyond the end of the visible 20 characters. You should know how to find all this information.

It is also possible to design your own font for use with this particular display. All the information needed to do so can be found in the Hitachi HD44780U manual/datasheet.

1. Write a program to put the 26 letters of the alphabet and the ten numerals in the 40 spaces that are available on the display. Put four spaces between the numbers and the alphabet to fill in the four remaining spaces. Once all the characters have been entered, scroll the 40 characters back and forth endlessly though the two lines of the display.

2. Write a program to bubble the 26 capital letters of the alphabet through the numbers 0 to 9 on line two of the LCD. (This means: First put the numbers on line two. “A” then takes the place of the “0” and all the numbers move over. Then the “A” takes the place of the “1” and the “0” moves to position 1. Afterward, the “A” replaces the “2” and so on till it gets past the 9. Following this, the “B” starts its way across the numbers and so on.) Loop forever.

3. Write a program to write the numbers 0 to 9 upside down on line 1. Wait 1 second and then flip the numbers right side up. Loop.

4. Create a program to write “HELLO WORLD” to the display and then change it to lowercase one letter at a time with 100 milliseconds between letters. Wait 1 second and go back to uppercase one character at a time with negative letters (all dots on the display are reversed to show a dark background with white letters in lowercase). Loop.

MISCELLANEOUS EXERCISES

These exercises are designed to challenge your programming ability. Again, you will need access to the datasheet for the LC Display.

1. Editor: Write a program that displays a random 12 numbers on line 1 of the LCD and displays a cursor that can be moved back and forth across the 20 spaces with potentiometer 0. The entire range of the potentiometer must be used to move across the 20 spaces. Allow the keypad to insert numbers 0 to 9 into the position the cursor is on. Assign a delete switch and an insert space switch on the keyboard. A comprehensive number (plus decimal and space) editor is required.

2. Mirror: Write a program that puts a random set of letters and numbers on line 1 and then puts their mirror images on line 2. The mirror is between line 1 and line 2. You have to learn how to create the upside-down numbers from the Hitachi datasheet for the display, and also learn how to read what is in the display from the display ROM.

3. Forty Characters: The display ROM is capable of storing 40 characters on each line. Design a program to allow you to scroll back and forth to see all 40 characters on both lines one line at a time. Use two potentiometers for scrolling, one for each line.

4. Four lines: Write a program to display four lines of random data on the LCD and to scroll up and down and side to side to see all four lines in their entirety. You have to store what is lost from the screen before it is lost so you can re-create it when you need it.

5. Bargraphs: Create a three-bargraph display, with each bar 3 pixels high, that extends across both lines of the LCD. The lengths of the bargraphs are determined by the settings of the three potentiometers, which change as the potentiometers are manipulated.

By now, you should be getting pretty good at using the 16F877A and are nearly ready to finish the introduction. Only a little more and we will be ready for just that!