PIC Basic Vs Assembly and Code Tutorial
PIC Basic Vs Assembly and Code Tutorial
1 CONDITIONAL EXPRESSIONS
1.1 THE BASICS
For a program to be useful, it must be able to make decisions. Is one variable greater than another is? Are two variables equal or not equal? Which part of the program should it jump to next? Is a bit set or cleared? Applying one or more of these questions to the contents of a microcontrollers registers and acting on the results make a program.
> (expression2) THEN (DoSomething) < (expression2) THEN (DoSomething) >= (expression2) THEN (DoSomething) <= (expression2) THEN (DoSomething)
In PIC assembly language there are no relational operators (=, <>, >=, etc.) for doing comparisons you must look at the Instruction Set Summary for the microcontroller you are using and find an instruction which will cause something distinct to happen when the comparison is made.
2 1.3.a SUBTRACTION
Subtraction with a PIC microcontroller is very simple. Load the W register (the working register) with the number you wish to subtract.
Example: movlw
Or
0x03
movf
reg1, 0
Then subtract the value in the W register from another register with this instruction:
subwf
1.3.b THE CARRY BIT
reg2,0
Now that we have performed our subtraction, we can test for magnitude or equality by checking the bits of the STATUS register.
If the value in W was greater than the value in reg2 then the Carry bit in the STATUS register will be clear. (i.e. C=0; the result was negative) If the value in W was less than or equal to the value in reg2 then the Carry bit in the STATUS register will be set. (i.e. C=1; the result was positive or zero)
This last statement is a bit of a disappointment. It implies that we need to rule out the possibility of the registers being equal which would require an extra step, extra code, and extra clock cycles to complete the comparison. In reality since you are writing the code, and presumably you know what you are trying to accomplish, you can change any less than comparisons into greater than comparisons. If A is less than B obviously, B is greater than A. You can do this by loading B into the W register then subtract it from A. 1.3.c THE ZERO BIT If youve been thinking ahead, you can see the other problem this creates. If you are testing for equality, you must rule out the possibility of one register being less than the other. Thankfully, the designers of microcontrollers realized that we would need an easy way to test for equality, and they provided a separate bit in the STATUS register: the Zero bit.
If the result of a subtraction equals exactly zero, the Zero bit will be set in the STATUS register (Z=0; the result is zero)
WARS PIC Assembly Programming Guide 1.3.d ALTERNATIVES TO SUBTRACTION The PIC microcontrollers with 14 bit cores have no alternative to subtraction when testing for greater than or less than.
If the contents of reg1 equaled 0x03 the Zero bit would now be set in the STATUS register. Similarly, to compare two registers to each other:
If the contents of reg1 equaled the contents of reg2, the Zero bit would now be set in the STATUS register.
If the contents of reg1 equaled the contents of reg2, the Zero bit would now be clear in the STATUS register.
If the contents of reg1 equaled zero then the Zero bit would be set in the STATUS register.
WARS PIC Assembly Programming Guide The W register cannot be tested this way because it is not an addressable register. However, since the value of the W register is always known following the execution of an instruction it does not need to be tested.
BTFSC Bit Test F Skip if Clear BTFSS Bit Test F Skip if Set
These two instructions allow you to test a bit in any addressable register and skip the next instruction if the bit is clear or set depending on which instruction you use.
Example:
or
btfsc status, Z ; Test the Zero bit in STATUS register goto somewhere ; Skip this line if Z=0
Example:
btfss status, Z ; Test the Zero bit in STATUS register goto somewhere ; Skip this line if Z=1
As in these examples, the line following the bit test is usually a jump instruction. Since the BTFSS and BTFSC instructions only skip one line, you need to jump over the block of code that will be executed if the bit test fails. We will see more of this in the following section.
Assembly Code movf xorwf btfss goto movlw movwf goto movlw movwf _endif: _a,0 _b,0 status, Z $+4 0x01 _a _endif 0x02 _a
In this example, you can see that it generally takes many assembly language instructions to make up one higher-level expression. You can see which parts of the BASIC code relate to its Assembly language counterpart by looking at the color-coding.
(rest of program)
This code fragment also demonstrates how to jump over small sections of code using the $ operator. The $ symbol refers to the current value of the program counter. So a statement like:
goto $+4
means goto here plus four instructions. Which, in this case points to the line containing:
movlw 0x02
which is the beginning of our Else clause. This saves us from having to come up with labels for each jump we make. For instance the next time we had an IF statement we would have to come up with another _endif: label. We could use _endif2: or _endif3: but this could easily get out of hand in a large program. 2.1.b AN INEQUALITY To see how the code would change here is an example of an inequality:
Assembly Code movf xorwf btfsc goto movlw movwf goto movlw movwf _endif: _a,0 _b,0 status, Z $+4 0x01 _a _endif 0x02 _a
As you can see, only the bit test has changed. We are now using BTFSC instead of BTFSS to test the Zero bit of the STATUS register.
(rest of program)
WARS PIC Assembly Programming Guide 2.1.c MAGNITUDE COMPARISONS Examples of magnitude comparisons are as follows:
Assembly Code movf _a,0 subwf _b,0 btfsc status, C goto $+4 movlw 0x01 movwf _a goto _endif movlw 0x02 movwf _a _endif: (rest of prog.) Assembly Code movf _b,0 subwf _a,0 btfss status, C goto $+4 movlw 0x01 movwf _a goto _endif movlw 0x02 movwf _a _endif: (rest of prog.)
Assembly Code movf _b,0 subwf _a,0 btfsc status, C goto $+4 movlw 0x01 movwf _a goto _endif movlw 0x02 movwf _a _endif: (rest of prog.) Assembly Code movf _a,0 subwf _b,0 btfss status, C goto $+4 movlw 0x01 movwf _a goto _endif movlw 0x02 movwf _a _endif: (rest of prog.)
As you can see, the code remains almost entirely the same for each of the examples. Notice that the (a<b) example is actually implemented as (b>a). The value in register B is subtracted from the value in register A, and the test checks for (b>a). Similarly, since a set Carry bit indicates a less than or equal to relationship the (a>=b) example is implemented as (b<=a). 2.1.d COMPILER INEFFICIENCIES Since compiler writers have no way of knowing what you are trying to accomplish with your code, a low quality compiler might generate code with a test for equality and a separate test for magnitude. This would contain extra jump instructions and would slow a program down unnecessarily.
WARS PIC Assembly Programming Guide A side-by-side comparison of the right and wrong ways to implement (a>=b) shows this extra code:
Assembly Code Implemented As (b <= a) movf _b,0 subwf _a,0 btfss status, C goto $+4 movlw 0x01 movwf _a goto _endif movlw 0x02 movwf _a _endif: (rest of program)
Inefficient Assembly Code movf _a,0 subwf _b,0 btfsc status, C goto $+4 movlw 0x01 movwf _a goto _endif movf _a,0 xorwf _b,0 btfss status, Z goto $+4 movlw 0x01 movwf _a goto _endif movlw 0x02 movwf _a _endif: (rest of program)
The inefficient assembly code example is implemented as two separate tests and can be written in BASIC code as:
10
BASIC Code If ((a > b) AND (b < c)) OR (c <> a) then a = b endif
Assembly Code Implemented As: If (c <> a) then a = b else If (a > b) then If (c > b) then a = b endif movf _a,0 xorwf _c,0 btfss status, Z goto $+9 movf _b,0 subwf _a,0 btfsc status, C goto _endif movf _c,0 subwf _b,0 btfsc status, C goto _endif movf _b,0 movwf _a _endif: (rest of program)
Once again, the colors indicate the relative sections of the code. The OR section is performed first followed by the AND section, and again (b < c) has been implemented as (c > b). 2.2.a NEGATIVE LOGIC Notice I have made a departure from the previous programming formula of:
10
WARS PIC Assembly Programming Guide 2.2.b USING A HIGHER LEVEL LANGUAGE AS COMMENTS
11
With the color coding it is easy to tell what each piece of code does. If you wrote a program using this code, I doubt you would be color-coding it, and you would quickly forget what you were trying to accomplish. When you write out your code, comment the code with the BASIC code. BASIC is self-explanatory so you can easily remember what you were trying to do.
11
12
switch(expression) { case (value1): statement_1; . . . statement_N; break; case (value2): statement_1; . . . statement_N; break; . . . case (value N): statement_1; . . . statement_N; break; default: statements; }
12
13
The (expression) part of the above psudo-code evaluates to an integer. This integer is used as an index to determine which case block is executed. The optional break keyword is used to terminate each case block. If the current case block does not include a break keyword, program execution continues into the next case block. The optional default section of the psudocode is executed if there is no match to the (expression). If there is no match to (expression), and the default section is omitted, then no part of the switch body is executed. 3.1.a DEALING WITH SMALL MICROCONTROLLERS Since integers are usually two bytes long, (16 bits 16,536 possibilities) and PIC microcontrollers have small program code memories, the switch (expression) is usually implemented as an 8 bit (256 possibilities) character (char) data type. You will probably never even come close to using a 256 case SELECT statement.
3.2 IMPLEMENTATION
3.2.a LINEAR SEQUENCE OF COMPARISONS The simplest way to implement the CASE statement is as a linear sequence of comparisons against each arm in the statement.
If (expression = 1) then statement1 endif If (expression = 2) then statement2 endif If (expression = 3) then statement3 endif If (expression = 4) then statement4 endif
This technique is inefficient for even a small number of CASE statements. The value of the expression is tested against a constant for each IF statement even if the expression has already matched a previous IF statement. For large numbers of IF statements this can take many clock cycles. 3.2.b THE IF-TREE A more sophisticated technique is the if-tree, where the selection is accomplished by a nested set of comparisons organized into a tree.
If (expression = 1) then statement1 else If (expression = 2) then statement2 else If (expression = 3) then statement3 else If (expression = 4) then statement4 endif
13
14
WARS PIC Assembly Programming Guide This technique is more efficient then the previous technique because the comparisons stop when a match is found. This technique is efficient for a small number of CASE statements, but the number of possible comparisons increases linearly with the number of CASE statements.
3.2.c THE JUMP TABLE A more common implementation is the jump table. In this approach the
Case_Table:
goto case_0 goto case_1 goto case_2 goto case_3 goto case_4 goto case_5
In this example, if the (expression) evaluated to three, the code in location 3 would be executed. This results in a jump to case 3s code. The advantage to this approach is the execution time of this section of the code is constant regardless of the number of cases. Its main disadvantage is that the case indices must be consecutive.
14
15
Here is an example of how to implement the SWITCH statement in a PIC microcontroller with a 14-bit instruction set:
C Style Code switch (3) { case 0: statement1; statement2; break; case 1: statement1; statement2; break; case 2: statement; case 3: statement1; statement2; break; default: statement1; }
_case_table:
_case0:
_case1:
_case2: _case3:
_default:
Assembly Code movlw 0x03 ;Load the expression call _case_table ;value and call the table nop ;This is where the program . ;resumes execution . . . addwf PCL ;add the expression goto _case0 ;value to the program goto _case1 ;counter and go to goto _case2 ;the address of the proper goto _case3 ;case statement goto _default . . . statement1 statement2 return ;jump back to main program statement1 statement2 return ;jump back to main program statement ;Notice lack of return statement1 ;case2 continues into case3 statement2 return ;jump back to main program statement1 return ;jump back to main program
This example has the CASE table code separate in memory from the code for the case statements. If the case statements immediately followed the CASE table the goto instruction could have been changed to the goto $+(offset) form:
15
16
_case_table:
Assembly Code 0x03 ;Load the expression _case_table ;value and call the table ;This is where the program . ;resumes execution . . . addwf PCL ;add the expression goto $+5 ;value to the program goto $+7 ;counter and go to goto $+9 ;the address of the proper goto $+9 ;case statement goto $+11 statement1 statement2 return ;jump back to main program statement1 statement2 return ;jump back to main program statement ;Notice lack of return statement1 ;case2 continues into case3 statement2 return ;jump back to main program statement1 return ;jump back to main program movlw call nop
This leads to code that is fast, compact, efficient, easy to read, and almost as concise as the high-level language equivalent.
16
17
This automatically turns any number less than a number in the valid range into a number greater than a number in the valid range. Our range can be determined by subtracting the lower bound from the upper bound (90 65 = 25). Our range test now becomes:
17
18
Sometimes programmers are tempted to jump into the middle of a section of code to save space or time. This can help in the short term but the code ends up being very hard to follow and the structure is very hard to see. For example, look at this SWITCH statement generated by the HIGH-TEC PICC compiler:
Address 03DD 03DE 03DF 03E0 03E1 03E2 03E3 03E4 03E5 03E6 03E7 03E8 03E9 03EA 03EB 03EC 03ED 03EE 03EF 03F0 03F1 03F2 03F3 03F4 03F5 03F6 03F7 03F8 03F9 03FA 03FB 03FC 03FD 03FE 03FF
case1
case2 case3
default
start
end
Assembly Code goto start clrf _a clrf _b incf _b goto end clrf _a incf _a movlw 0x2 goto 0x3EC movlw 0x2 movwf _a movlw 0x3 movwf _a goto 0x3DF movlw 0x8 movwf _b goto end movf _a, W movwf _temp movlw 0x4 subwf _temp, W btfsc STATUS, C goto default movlw 0x3 movwf PCLATH movlw 0xFB addwf _temp, W btfsc STATUS, C incf PCLATH movwf PCL goto case0 goto case1 goto case2 goto case3 goto exit
Comments ;a=0 ;b=1 ;Could have jumped to exit directly ;a=1 ;Spaghetti code ;a=2 ;a=3 ;Spaghetti code ;b=8 ;Could have jumped to exit directly ;Save a in temp ; ;Perform range ;test ;
18
19
Without the comments and shading you probably could have guessed it was a SWITCH statement but the two spaghetti-code goto instructions make it hard to see the individual cases. Here is the C code it was generated from:
main() { char a, b; switch (a) { case 0: a=0; b=1; break; case 1: a=1; b=2; break; case 2: a=2; case 3: a=3; b=1; break; default: b=8; } } 3.2.c3 THE WRAP-AROUND PROBLEM
The only danger to this method occurs when you are using more than 127 cases. After 127 cases, it is possible to wrap-around into the valid range. At this point, you would have to explicitly compare the number to the upper and lower bounds as in the preceding example.
If (expression < lower_bound) OR (expression > upper_bound) then goto _default endif
19
20
switch (expression) { case 0: statement1; break; case 20: statement2; break; case 50: statement3; break; }
Using the jump table method you would use 51 memory locations to index three statements. In this case, it is better to use an If-Tree approach:
If (expression = 0) then statement1 else If (expression = 20) then statement2 else If (expression = 50) then statement3 endif
Consider the following situation:
switch (expression) { case 0: statement1; break; case 50: statement2; break; case 51: statement3; break; case 52: statement4; break; case 53: statement5; break; case 54: statement6; break; case 55: statement7; break; }
20 Winnipeg Area Robotics Society
WARS PIC Assembly Programming Guide In this situation, a jump table would require 56 memory locations to index seven statements, and an If-Tree would be getting quite large.
21
In this situation, it is possible to combine the approaches to yield an optimal solution. Notice that cases 50 to 55 are consecutive and case 0 is the only non-consecutive case. Use an IF statement to test for case 0 and a SWITCH statement for cases 50 to 55.
If (expression = 0) then goto statement1 endif switch (expression 50) { case 0: statement2; break; case 1: statement3; break; case 2: statement4; break; case 3: statement5; break; case 4: statement6; break; case 5: statement7; break; }
Notice that in the SWITCH statement we subtracted 50 from the expression value to translate a range from 50 to 55 down to 0 to 5. Also, note that if the expression were equal to zero there would be no need to enter the SWITCH statement. You would then have to include some code to jump over the SWITCH part of the code. 3.2.d ASSEMBLER VS. COMPILER Examining the expected data and optimizing the implementation is a technique usually only found in compilers for larger more popular microprocessors. Most PIC compilers would not bother with this optimization and would implement every SWITCH statement as an If-tree. Once again, the assembly programmer has a chance to outperform the compiler by having the flexibility to choose implementations.
21
22
4 LOOPS
After the ability to make decisions, the ability to loop is the most important characteristic of a program. Loops are used for time delays, comparing strings, generating cyclic redundancy checks, shifting in serial data, and many other applications. Almost every program contains some type of loop. There are many types of loops but they all have a few elements in common. There is an optional initialization section, a loop termination test, and the body of the loop. Common types of loops in high-level languages are:
loop:
A microcontroller usually performs some continuous task while it is turned on even if it is just waiting for in input. This task never ends so it is incorporated within an infinite loop.
22
23
statement statement btfsc STATUS, Z goto rest_of_prog statement goto loop rest_of_prog: statement
loop:
;Test for some condition ;Break out of the loop if condition is false
23
24
WARS PIC Assembly Programming Guide In the C programming language, WHILE loops take this form:
a = 5; while (a) { a = a 1; }
This loops exactly five times. On the sixth time a = 0 which is defined as FALSE so the loop immediately exits. In PIC assembly language:
Assembly Code movlw 0x05 movwf _a _loop: movf _a, 1 btfsc STATUS, Z goto _exit decf _a goto _loop _exit: rest of program ;a=5 ;move a to itself to set or clear Z bit ;test if it was zero ;a = a 1 (loop body)
The termination conditions can be tested using the methods described in the Conditional Expressions chapter, and the loop is created by a goto instruction that jumps backwards in program memory.
WARS PIC Assembly Programming Guide In the C programming language, the DOWHILE loop takes this form:
25
do { a = read(portb); switch (a) { case 0: break; case 1; display_temperature(); break; case 2: display_humidity(); break; } }while (a < 3);
25
26
WARS PIC Assembly Programming Guide With a WHILE loop you would have no idea what the value of port B was so you would have to check before you entered the loop, and again inside the loop.
a = read(portb); while (a < 3) { switch (a) { case 0: break; case 1; display_temperature(); break; case 2: display_humidity(); break; } a = read(portb); }
In PIC assembly language the DOWHILE loop is very similar to the WHILE loop, only the location of the loop termination condition changes.
Assembly Code movlw 0x05 movwf _a _loop: decf _a movf _a, 1 btfss STATUS, Z goto _loop rest of program ;a = 5 ;a = a 1 (loop body) ;move a to itself to set or clear Z bit ;test if it was zero
Notice that when the loop termination statement is encountered for the first time the value of a is 4. This may lead to some confusion. Does the loop execute 4 times or 5 times? The answer is that it has already executed once by the time it reaches the termination statement so it will execute 4 more times, to make 5 times in total. 4.4.b THE OPTIMAL LOOP STRUCTURE Another important thing to notice is that because the termination condition was moved to the end of the loop we could eliminate an extra goto instruction. This is the optimal loop structure. The extra goto instruction slowed down our loop by two clock cycles in every iteration of the loop. This can add up very fast in a microprocessor and lead to sluggish performance, especially in real-time applications.
26
WARS PIC Assembly Programming Guide 4.4.c OPTIMIZING THE WHILE LOOP
27
The WHILE loop can be optimized by converting a DOWHILE loop into a WHILE loop with an extra goto instruction:
Assembly Code movlw 0x05 movwf _a goto _test _loop: decf _a _test: movf _a, 1 btfss STATUS, Z goto _loop rest of program ;a = 5 ;a = a 1 (loop body) ;move a to itself to set or clear Z bit ;test if it was zero
At first, this doesnt look like weve accomplished anything. The extra goto instruction is only performed once, upon entering the loop, and the rest of the loop is identical to the DOWHILE loop. The resulting code is marginally harder to follow, but if you always write your WHILE loops this way it should be easily recognized.
27
28
WARS PIC Assembly Programming Guide The STEP keyword is optional in the BASIC example. If STEP is not included, the default step size is +1. The entire contents of the C language FOR expression are optional. Here are two typical FOR loops:
the loop counter variable j is initialized to 1 the loop continues until j > 20 j increments at the end of the loop.
Assembly Code clrf _j incf _j _Loop: movf _j, 0 xorlw 0x15 btfsc STATUS, Z goto _end loopbody incf _j goto _Loop: _end rest of program ;j = 1 ;is j decimal 21 ;if yes then end ;increment loop counter
Since we know that j is not greater than 20 the first time through the loop (because we initialized it to 1), we can move the termination test to the end of the loop:
Assembly Code clrf _j incf _j _Loop: loopbody incf _j movf _j, 0 xorlw 0x15 btfsc STATUS, Z goto _Loop: _end rest of program ;j = 1 ;increment loop counter ;is j 21 (decimal)
28
29
If the loop counter variable is not being used inside the loop body, you can further optimize the FOR loop by having it count backwards. This is the equivalent to: FOR j = 20 to 1 STEP 1.
Assembly Code Implemented As: FOR j = 20 to 1 STEP 1 movlw 0x14 movwf _j ;j = 20 _Loop: loopbody decf _j ;decrement loop counter movf _j, 1 btfss STATUS, Z ;is j = 0 goto _Loop: _end rest of program
The FOR loop still executes 20 times, but now it does it in reverse. Running the loop counter down to zero eliminates the need to subtract or XOR a literal value with the W register. We can now simply check the ZERO bit of the STATUS register when the loop counter decrements. By performing both optimizations, we have saved three instruction cycles per iteration. Moving the termination condition to the end of the loop saved two instruction cycles by eliminating a goto instruction, and running the counter backwards saved one instruction cycle by eliminating a subtraction. 4.5.c USING STEP SIZES OTHER THAN +/- 1 The technique of counting down to zero doesnt work with non-unity step sizes unless the loop count is evenly divisible by the step size. The increment and decrement instructions incf and decf cannot be used. A literal (the step size) must be added or subtracted from the loop counter on each iteration.
_Loop:
_end
Assembly Code clrf _j incf _j ;j = 1 loopbody movlw 0x03 addwf _j ;increment loop counter movf _j, 0 sublw 0x14 ;is j > 20 (decimal) btfsc STATUS, C goto _Loop: rest of program
29
30 So lets cheat:
Assembly Code movlw 0x15 movwf _j loopbody movlw 0x03 subwf _j movf _j, 1 btfsc STATUS, Z goto _Loop: rest of program ;j = 21
_Loop:
_end
So how did we come up with this? Heres how: The forwards loop executed until it reached (1 + (X * 3)) > 20. If you find the value for X you know how many times the loop will execute, which in this case was 7. If you start counting backwards from (X * 3) = (7 * 3) = 21 you will reach zero in the same number of iterations. Doing all this work to save one instruction cycle per iteration usually isnt worth it. In most cases, youll never see the difference between the previous two pieces of code. Chances are if youve picked a non-unity step size, you are using the loop counter value somewhere inside the loop. If that is the case, you probably cant run the counter backwards anyway.
30