In this
article:
Subtraction
Conclusion
Navigation:
HomeHardware
Software
Techniques
Controllers
Reviews
Index
Introduction
Performing arithmetic operations on a computer is a given, however arithmetic operations have a precision that is bound by underlying instruction set of the CPU. If you want more precision you are generally stuck writing programs to simulate greater precision by building up an operation from a series of lower precision ops. This is very true for the PIC16F628 microcontroller.
This is a useful thing to do and Microchip published an AppNote on how to do it. But when I converted it into macro form I inadvertently added a bug (or failed to fix it) which my friend Mike Albaugh discovered. The bug is that carry wasn’t being preserved by my macros so depending on the state of the flags after using the macro worked mostly, but in specific cases failed to preserve carry. This article looks at the fix.
Basic Arithmetic
What we are talking about here is implementing arithmetic, which I think of as sort of the functional part of math. The operators of add, subtract, multiply, and divide. As the PIC16 architecture uses 8 bit registers, we have to do multiple precision arithmetic the same way we learned it in grade school, the long way. Except in school we added single digits and carried or borrowed from one digit to the next, and in computer arithmetic we use single bytes and carry to, or borrow from, the next one up the chain. The mechanics of multibyte arithmetic are described fairly well in the Application Note but the nuances of the PIC chip are what bite you.
Adding two numbers
The first operation is addition. We tackle that one because it is both the easiest to understand mathematically and it is the easiest one to understand programmatically. This is how two byte addition is performed in the Application Note.
; ; Double Precision Addition ( ACCb + ACCa -> ACCb ) ; From AN526 ; D_add: MOVF ACCaLO, W ADDWF ACCbLO, F ; add lsb BTFSC STATUS, C ; add in carry INCF ACCbHI, F MOVF ACCaHI, W ADDWF ACCbHI, F ; add msb RETLW 0
Lets go over how this works. Move the least signifcant byte of the first variable (ACCa in the code) into the W register, then add the least significant byte of the second variable (ACCb), and overwrite it.
We note here that the ADDWF instruction sets both the carry and zero flags so if adding the two numbers resulted in a bit being ‘carried out’ the C flag in the STATUS register is set, and if similarly if the result ended up being zero the Z flag is set.
If the carry occured then the next step is to add the two high bytes and +1 which represents the carry. That is where the BTFSC (Bit Test F Skip if Clear) instruction comes in. It tests the state of the C bit in the STATUS register. If it is clear (indicating no carry) it skips the next instruction, otherwise the next instruction is executed. That instruction is the INCF instruction.
The INCF does the add of 1, which is needed in the case of carry, to the soon to be overwritten high byte of ACCb. We follow that by putting the high byte of ACCa into W and then adding it and the potentially modified version of ACCb and storing it into ACCb. Then return back to the caller.
Macrotizing the ADD
So I (and others) used this snippet of code to create macros for adding two 16 bit numbers. I use them in my 16bits.inc file which has gone into a couple of projects. My original macro is shown below.
; ; 16 bit addition macro with carry out. ; Operation: SRC + DST => DST ; ADD16 MACRO SRC, DST MOVF (SRC),W ; Get low byte ADDWF (DST),F ; Add to destination MOVF (SRC)+1,W ; Get high byte BTFSC STATUS,C ; Check for carry INCF (SRC)+1,W ; Add one for carry ADDWF (DST)+1,F ; Add high byte into DST *loses carry if W is zero* ENDM
You can recognize the same general series of steps in the macro but there are some subtle differences. First the obvious difference is no return intruction because this code will be injected inline, but the other is that the loading of the high byte of the destination into the W register before the carry test. Finally, the macro comments say (and it was intended) that the carry flag was accurately set at the end of the macro. This is different than the Microchip code which is silent on the issue.
I used this macro in my speed controller and elsewhere but when a friend of mine, Mike Albaugh, tried to use the code he noted it didn’t always leave the carry bit set correctly. Mike was trying to do even greater than 16 bit arithmetic and that just didn’t work if you lose the carry bit.
The bug turned out to be that INCF can result in the high byte of the SRC being zero when there was a carry from the low byte, and the high byte of the source was initially 0xFF. Because of the carry, the code increments the high byte, and since it was 0xFF and it gets incremented to 0x00.
The effect of that is two fold, the first is that the W register contains zero because we should have “carried out” of the high byte, the second is that the ADDWF with a 0 in the W register resets the carry flag. Presto chango, loss of carry accuracy. Adding a value like 0xFF00 to 0x01FF gave you 0x00FF with no carry!
In fact it got weirder than that, consider these conditions for SRC + DST => SRC;
- 0xFF01 + 0x01FF => 0x0100 with no carry
- 0x01FF + 0xFF01 => 0x0100 with carry.
- For any value where the two low order bytes would generate a carry and the high order byte of the SRC value was 0xFF there was no carry generated, but if the high order source byte was not 0xFF carry was correctly generated.
The problem was that pesky ADDWF resetting the carry when the high byte of the source rolled over to zero. And that suggested the fix.
The updated code is shown below, the only change was to replace the INCF instruction with an INCFSZ instruction. The two insights that Mike had were that first, the INCFSZ instruction would not reset the Carry flag that had to be set in order for that instruction to be executed. And secondly, if that instruction did result in the high order byte becoming zero the ADDWF was going to be a NOP anyway so skipping it is safe, the destination high byte won’t change.
; ; 16 bit addition macro with carry out. ; Operation: SRC + DST => DST ; ADD16 MACRO SRC, DST MOVF (SRC),W ; Get low byte ADDWF (DST),F ; Add to destination MOVF (SRC)+1,W ; Get high byte BTFSC STATUS,C ; Check for carry INCFSZ (SRC)+1,W ; Add one for carry ADDWF (DST)+1,F ; Add high byte into DST ENDM
If you run through this in your head or with the simulator you can see that it does indeed leave Carry correctly set for values that include 0xFF in the high byte of the SRC operand. But what about subtract? Did it have a bug?
Subtraction
Of course it did, only in reverse. Unlike addition the role of the carry flag is to indicate a “borrow”, which is to say that initially it is set to 1 on a subtraction and if the subtracted value is less than the source it ‘borrows’ one from the carry flag.
Microchip wasn’t as helpful here since in their appnote they add a negation routine and simply call negate and then fall through to their add. If we’re using macros that makes the subtract call twice as long as it needs to be as it does the negation and then the addition.
The original subtraction macro code from my 16bits.inc file is shown below.
; ; 16 bit subtraction: DST - SRC => DST ; Carry is preserved. ; SUB16 MACRO DST, SRC MOVF (SRC),W ; Get low byte of subtrahend SUBWF (DST),F ; Subtract DST(low) - SRC(low) MOVF (SRC)+1,W ; Now get high byte of subtrahend BTFSS STATUS,C ; If there was a borrow, rather than INCF (SRC)+1,W ; decrement high byte of dst we inc src SUBWF (DST)+1,F ; Subtract the high byte and we're done ENDM
One thing to remember here is that SUBWF can be a very non-intuitive instruction. It subtracts the “W” register from the “F” location, and then leaves the result either in W or F. That means “Subtract whatever you are holding in W from this thing I’m specifying as the operand.” Since I am used to the operand being the thing that is subtracted (not the other way round) it makes me look twice.
SUBWF also sets the Carry, Decimal Carry, and Zero bits. So our first two instructions load the low order byte of SRC into W then subtract it from the low order byte of DST, leaving the result in the low byte of DST. Also carry is now clear if the original SRC low byte was larger than the DST low byte.
Like ADD16, the next step loads the high order byte from SRC into W preparing to subtract it from the high order byte of DST. The state of the Carry bit is checked with the BTFSS (Bit Test F Skip if Set).
If C is set, then no borrow occured, and the next instruction is skipped. If the C bit is not set, then a borrow did occur and the next instruction has to fix up the numbers taking into account the borrow. It can do this in one of two ways, one it can DECF (decrement by 1) the high order byte of DST, or it can increment the high order byte of SRC (the subtrahend). This is because (A - 1) - B is the same as A - (B + 1). And on the PIC, as we’ve seen in ADD if there is an overflow on the borrow or carry, the only way to catch it is by a register going to zero.
In the case of ADDWF we caught that because adding 1 to 0xFF made it roll over to zero but if you subtract 1 from 0x00, you get 0xFF which is indistinguishable from subtracting one from any other value other than 1 (which goes 1 -> 0 on a subtract). Thus adding one to SRC will set Z if SRC was 0xFF and adding 1 made it 0x00. That gives us the overflow detection we need.
But it has the same bug that ADD16 did which is this, if SRC+1 (the high byte of the SRC variable) is 0xFF, and we increment it, then it becomes 0 and the following SUBWF leaves the CARRY bit set since subtracting 0 from something will never cause a borrow! So like Add, the increment has to skip the next instruction if the result was zero and leave CARRY what it was (not set) and it can safely skip the subtract because subtracting 0 won’t change DST+1.
The updated code is here:
; ; 16 bit subtraction: DST - SRC => DST ; Carry is preserved. ; SUB16 MACRO DST, SRC MOVF (SRC),W ; Get low byte of subtrahend SUBWF (DST),F ; Subtract DST(low) - SRC(low) MOVF (SRC)+1,W ; Now get high byte of subtrahend BTFSS STATUS,C ; If there was a borrow, rather than INCFSZ (SRC)+1,W ; decrement high byte of dst we inc src SUBWF (DST)+1,F ; Subtract the high byte and we're done ENDM
Conclusion
Well the simple conclusion is that doing math on the PIC is non-intuitive. Fortunately once you get the code written, if you are sure it works you probably won’t have to change it again.