Prac 2: Counter
• Practice using data movement and control flow statements
• Understand instruction timing and digital IO operations.
• Write PIC assembly code to display a counter using the 7-segment display board on the PIC trainer
• PIC Trainer and ICD 3
• Multiplexing board
• MPLAB X IDE software
Background documents (on FLO)
• MPASM Assembler Users Guide and PIC18F452 Instruction Set Summary
• PIC Trainer board schematic and multiplexing board schematic
Controlling the Multiplexing Board
The 7-segment displays on the MUX board are controlled by writing an appropriate value to each bit of PORTB to specify the state of each display segment (1 is on, 0 is off), as indicated in the figure. Thus to turn on the middle segment (segment g), you would set bit 1 of PORTB.
You can turn each digit display on setting bit 5 of PORTC (for the right-hand display) or bit 4 (for the left-hand display). Since PORTB controls the segments of both displays in parallel, you can show only one pattern at a time.
The DIP switches on the MUX board are connected to PORTD via a multiplexor. Bit 1 of PORTC selects which switch (low selects S1 and high selects S2), and bit 0 of PORTC enables the multiplexor (active low). The value of the selected switch bank is then available on PORTD. Since both switches are connected to PORTD, you can read only one switch at a time.
Note that because the 7-segment displays use PORTB (to control the segments), there is a conflict with the use of the debugger, which also uses some of the bits of PORTB (to control program execution).
Specifically, if you debug a program that uses the display, segments a and b will always appear off. The conflict does not occur if the program is run normally.
Checkpoint 1: Getting Started
1. Obtain a multiplexing daughter board with dual 7-segment display and dual DIP switches and carefully plug it into the main board, making sure that the board is oriented correctly and the pins are aligned properly with the jacks. Set the DIP switches on the main board so that all switches are off.
2. Create a new MPASM project named P2_Counter, making sure that project files are located in the MPLAB X projects folder on your U drive, and that the project configuration is correct for the PIC Trainer. Create a new project source file named counter.asm. Make sure that the configuration bits are set correctly for the trainer board.
3. Write a subroutine named init to initialise the PIC Trainer so that PORTB and PORTC are configured for output, and PORTD for input. To configure an IO bit for output, set the appropriate TRIS special function register bit to 0; to configure it for input, set the TRIS register bit to 1. For example, to configure all bits of PORTB for output you could write clrf TRISB
4. Write a main program that calls init, then turns on segments a to f (but not g) of the righthand display, thus displaying digit '0'. The program should end in an -idle- loop.
org 0x0000 start: call init
movlw .... ; segments to display '0'
movwf PORTB bsf PORTC, 5 idle: bra idle
;;; initialise PIC trainer to use MUX board
initialisation code return
Checkpoint 2: Single Digit Counter
5. Write a subroutine named get_hex_7seg that returns in WREG the 7-segment bit pattern necessary to display the hex digit corresponding to the low nibble of the input value in WREG. For example, to display '8' you need to turn on all segments, and to display '1' you need to turn on just segments b and c. The simplest approach is to use a lookup table; mask WREG to remove the high nibble, then use the result to select which value to return. The lookup macro (defined in the file table.inc, which you'll need to include) will do the lookup if you provide it with an appropriate table of values. You will need to define segment patterns for all digits from '0' to 'F'.
;;; return 7-segment code for low nibble of WREG
;;;----------------------------------------------------- get_hex_7seg: andlw h'0f' ; mask off high nibble lookup HEX_7SEG_CODES ; do the table lookup
;; 7-segment codes for hex digits 0 .. F
;; bit assignments: 'abcdefg.'
HEX_7SEG_CODES: db b'11111100', b'01100000', b'11011010', b'11110010' ;0123 db ... ;4567 ...
6. Write a main program that displays the value of a variable named count, increments the variable, and repeats. Use cblock to declare a file register for count, and don't forget to initialise it to an appropriate starting value. To display the variable, fetch it into WREG, convert the value to the appropriate 7-segment bit pattern by calling get_hex_7seg, then send the pattern to the right-hand digit of the 7-segment display by writing the segment pattern to PORTB and setting the appropriate bit of PORTC.
initialisation code again: fetch count and convert to 7-segment pattern
send to display increment count bra again
7. To slow the count down so that you can see the digits change, you'll need to add some delay to the loop. For this checkpoint, you can use the delay_t macro (include the file delay.inc). For example, this code will introduce a delay of 1 second:
delay_t d'1000', msec, delay_counter
Note that you'll need to declare delay_counter:3 (a 3-byte variable) for the macro to use.
Checkpoint 3: Poor-man's Double Digit Counter
It's easy enough to write code to display the high nibble of a value. Just use swapf to exchange the nibbles, then use your existing code to display the bit pattern on the left-hand digit. But since both digits of the 7-segment display are controlled by PORTB, you can only display one digit at a time. To create the illusion of a 2-digit display, you need to rapidly switch between the appropriate patterns for the 2 digits.
8. A simple approach (but one that doesn't fully solve the problem) is to split the loop delay into 2 equal parts, with half of the delay following the code for each digit. Then each digit will be displayed for half of the time. again: compute high nibble pattern and display on left-hand digit
compute low nibble pattern and display on right-hand digit
half delay increment count bra again
Use this approach to implement a 2-digit counter with a count interval of 100 ms. The counter will flash badly, but you should be able to tell if it's working correctly.
9. Add code so that the counting can be controlled by the S1 DIP switches on the MUX board. S1.1 makes the counter run (low) or stop (high). S1.2 controls the direction of the count (low for up, high for down).
The simplest strategy is to use the switches to set the size of an increment that's added to count: -1 if S1.2 is high (count down), 1 if it is low (count up), and 0 if S1.1 is high (no change). You'll probably find the instructions btfss and btfsc useful.
test S1.2 and skip if low
set WREG to -1 test S1.2 and skip if high
set WREG to 1 test S1.1 and skip if low
set WREG = 0 add WREG to count
10. Finally, modify the code so that the counter counts in decimal rather than in hexadecimal. All you need to do is make sure the arithmetic (incrementing or decrementing) works in BCD. If you add two BCD values, the result may not be valid BCD because of carries between digits. However, the daw instruction will do the necessary adjustments to convert such a result back to the appropriate BCD value, so all you need to do is execute that instruction after every arithmetic operation. And, of course, you'll need to work out the appropriate BCD value for -1 so you can count down as well as up.
Checkpoint 4: A Better Double Digit Counter
A much better way to implement a 2-digit counter is by storing the patterns for the digits into variables, then writing a delay routine that uses a -worker- subroutine to fetch and display the patterns as needed. A worker routine is one that is called repeatedly, with each call doing the next small step in a ongoing -job-. In this case, the worker will be called from within the delay loop, and will simply alternate between the left-hand digit pattern and the right-hand digit pattern on successive calls, thus rapidly switching the digit display. again: compute low nibble pattern and store in digit_right var compute high nibble pattern and store in digit_left var
call delay_and_display increment count bra again
delay_and_display: initialise delay counter for appropriate length delay delay_again: call display_worker with current delay counter as parameter
decrement delay counter bnz delay_again return
display_worker: display either digit 0 or digit 1, based on bit 0 of WREG return
You can create a delay of a specified length by running a countdown loop with an appropriate loop count. To get long enough delays, you'll often need to use loop counts that are bigger than 1 byte. For example, here's how you could write a 2-byte count loop. For the PIC trainers, a 2-byte count can generate delays up to about a second, which will be enough for this prac.
;;; delay while displaying alternate left and right digits delay_and_display: movlw low(LOOP_COUNT) ; initialise 2-byte counter
movwf delay_counter + 0 movlw high(LOOP_COUNT) movwf delay_counter + 1 delay_again
movf delay_counter + 0, w ; worker value
clrf WREG ; 2-byte decrement
decf delay_counter + 0 subwfb delay_counter + 1
iorwf delay_counter + 0, w ; set Z if both bytes 0
iorwf delay_counter + 1, w
bnz delay_again return
To set the precise timing, you need to work out how many instruction cycles (and therefore how much time) the loop takes to execute, then compute the appropriate loop count to get the right total time. Of course, you'll also need to count the instructions in the worker, and don't forget to allow for the call and return. Note that some instructions (notably branches, calls, and returns) take 2 instruction cycles, and that a skipped instruction is actually executed as a NOP, so the timing of code that includes skips is independent of whether or not the skip is taken.
The PIC Trainer has a clock speed of 10 MHz and we're running in HS mode (which means that there are 4 clock cycles per instruction), so each instruction cycle takes 0.4 microseconds. You can assume (with very little error) that the time taken by instructions outside the delay loop is negligible.
11. Modify your program so that it uses the suggested structure. Set the loop counter in your delay loop so that the delay is 250 ms. Check that your timing is accurate by running the counter for a full cycle (a count of 100) and timing the duration with a clock or watch; it should run for exactly 25 seconds. To Explore Further
12. (Difficult) Use S1.3 to control the speed of the count. When the switch is high, the count proceeds at a faster rate.
13. (Challenging) Use S2 to control the length of the count cycle. The count begins at 0 and resets when it reaches the value read from S2. For example, if S2 is set to b'01100000' (BCD 60), the counter will count from 00 to 59 and then reset to 00.