diff --git a/Programs/Libraries/math.asm b/Programs/Libraries/math.asm index bb277fe..019b91a 100644 --- a/Programs/Libraries/math.asm +++ b/Programs/Libraries/math.asm @@ -54,8 +54,243 @@ int16sub: STQ ; Store the result. RET ; Return to the caller. +; ---- Multiplying, which this machine cannot do ---- +; +; There is no MUL. What there is instead is an identity: +; +; a * b = qs[a + b] - qs[|a - b|] where qs[n] is n squared over four +; +; because (a+b)^2/4 - (a-b)^2/4 is exactly a*b, and the halves that the flooring throws away +; cancel between the two terms. So a multiply is TWO LOOKUPS AND A SUBTRACT. +; +; ---- And the table is built by adding ---- +; +; Which is the part that makes this fit a machine with no multiplier at all. A table of +; squares would need squaring to fill, and this one does not: +; +; qs[n] = qs[n-1] + n/2 +; +; and n/2 goes 0, 1, 1, 2, 2, 3, 3 - a number that steps up on every even n. So the whole +; table is one running total and a counter, and nothing harder than an add appears anywhere in +; building the thing that does the multiplying. +; +; 511 entries of two bytes, because a and b are bytes and a+b reaches 510. That is 1,022 bytes +; of Data Memory, which is the price: this is a routine that trades a kilobyte for an +; operation the hardware has not got. + +; Fills the table. Call once, before the first multiply, and never again. +mulReady: + SETD.0 MulTable + RSTA + STA.0 + INCD.0 + STA.0 ; qs[0] is nought. + INCD.0 + + SETD.1 MulTotalLow + STA.1 + INCD.1 + STA.1 ; And so is the running total. + SETD.1 MulStep + STA.1 + SETD.1 MulToggle + STA.1 + + ; Five hundred and ten more entries to write. + INIA 0xFE + SETD.1 MulLeftLow + STA.1 + INIA 0x01 + SETD.1 MulLeftHigh + STA.1 + +mulEntry: + ; ---- The step goes up on every even index ---- + ; + ; n/2 for n = 1, 2, 3, 4 is 0, 1, 1, 2: it rises at the even ones. A toggle says which this + ; is, which is cheaper than halving a sixteen bit counter every time round. + SETD.1 MulToggle + LDA.1 + INIB 0x01 + XOR + STQ.1 + BNQ mulNoStep ; The toggle is one, so this index is odd and the step stands. + SETD.1 MulStep + LDA.1 + INCA + STA.1 +mulNoStep: + + ; The running total, plus the step, sixteen bits. + CCF + SETD.1 MulTotalLow + LDA.1 + SETD.2 MulStep + LDB.2 + ADD + STQ.1 + SETD.1 MulTotalHigh + LDA.1 + RSTB + ADD ; Nothing but the carry out of the half below. + STQ.1 + + ; And into the table, where DP0 has been walking all along. + SETD.1 MulTotalLow + LDA.1 + STA.0 + INCD.0 + SETD.1 MulTotalHigh + LDA.1 + STA.0 + INCD.0 + + ; One fewer to do, counted down sixteen bits. + SETD.1 MulLeftLow + LDA.1 + BNA mulCountLow + SETD.1 MulLeftHigh + LDA.1 + DECA + STA.1 + INIA 0xFF + SETD.1 MulLeftLow + STA.1 + BRI mulCounted +mulCountLow: + DECA + STA.1 +mulCounted: + SETD.1 MulLeftLow + LDA.1 + SETD.2 MulLeftHigh + LDB.2 + OR + BNQ mulEntry + RET + +; ---- One multiply ---- +; +; The operands go in MulA and MulB and the sixteen bit product comes back in MulLow and +; MulHigh. IN MEMORY RATHER THAN IN REGISTERS, because B cannot be stored and a product does +; not fit in one byte anyway - two registers in and two out would spend more instructions +; shuffling than the multiply costs. +mul8: + ; The sum, which is nine bits: two bytes can reach 510 between them. + SETD.0 MulA + LDA.0 + SETD.1 MulB + LDB.1 + CCF + ADD + BRC mulSumOver + RSTA + BRI mulSumHigh +mulSumOver: + INIA 0x01 +mulSumHigh: + SETD.0 MulSumHigh + STA.0 + MVQA ; Q is still the sum's low half; nothing above touched it. + SETD.0 MulSumLow + STA.0 + + ; The difference, without its sign, which is what the identity wants. + SETD.0 MulA + LDA.0 + SETD.1 MulB + LDB.1 + CCF + SUB + BRC mulOtherWay ; Borrowed, so B is the larger and the sum goes the other way. + MVQA + BRI mulDiffGot +mulOtherWay: + SETD.0 MulB + LDA.0 + SETD.1 MulA + LDB.1 + CCF + SUB + MVQA +mulDiffGot: + SETD.0 MulDiff + STA.0 + + ; qs[sum]. An entry is two bytes, so the index is doubled - one turn of the shift register, + ; where A is the high half and B the low, which is the way DPUW wants them too. + SETD.0 MulSumHigh + LDA.0 + SETD.0 MulSumLow + LDB.0 + SHL + SETD.0 MulTable + DPUW.0 + LDA.0 + SETD.1 MulLow + STA.1 + INCD.0 + LDA.0 + SETD.1 MulHigh + STA.1 + + ; Less qs[difference], sixteen bits, the borrow running from one half to the other. + RSTA + SETD.0 MulDiff + LDB.0 + SHL + SETD.0 MulTable + DPUW.0 + CCF + SETD.1 MulLow + LDA.1 + LDB.0 + SUB + STQ.1 + INCD.0 + SETD.1 MulHigh + LDA.1 + LDB.0 + SUB + STQ.1 + RET + #Data +; ---- The multiply's working ---- +; +; MulLow and MulHigh are next to each other on purpose: the product is read as a pair. +MulA: + 0x00 +MulB: + 0x00 +MulLow: + 0x00 +MulHigh: + 0x00 +MulSumLow: + 0x00 +MulSumHigh: + 0x00 +MulDiff: + 0x00 +MulStep: + 0x00 +MulToggle: + 0x00 +MulTotalLow: + 0x00 +MulTotalHigh: + 0x00 +MulLeftLow: + 0x00 +MulLeftHigh: + 0x00 + +; 511 entries of two bytes: n from nought to 510, which is as far as a byte plus a byte goes. +MulTable: + #Reserve 0d1022 + Int16operands: 0x00 ; Operand A high byte. 0x00 ; Operand A low byte. diff --git a/Programs/testPrograms/mulTest.asm b/Programs/testPrograms/mulTest.asm new file mode 100644 index 0000000..5262401 --- /dev/null +++ b/Programs/testPrograms/mulTest.asm @@ -0,0 +1,83 @@ +; The quarter square multiply, against numbers whose products are known. +; +; a * b = qs[a+b] - qs[|a-b|], and the table of quarter squares is built by adding, so nothing +; in the whole arrangement needs a multiply to exist. What this checks is that the arrangement +; actually multiplies: the cases below cover nought, one, the commutation, a square, and the +; largest product two bytes can hold. +; +; Written by Anachronaut + +#Include print.asm +#Include math.asm + +#Program + +start: + CALL mulReady ; The table, once, before anything asks for a product. + + RSTA + CALL times ; 0 x 0 + INIA 0d7 + SETD.0 MulA + STA.0 + RSTA + SETD.0 MulB + STA.0 + CALL show ; 7 x 0, which is the other way round from the last one. + + INIA 0d1 + CALL both + CALL show ; 1 x 1 + + INIA 0d12 + CALL both + CALL show ; 12 x 12, a square, which is the case the identity leans on: + ; the difference term is nought and the whole answer is one entry. + + INIA 0d3 + SETD.0 MulA + STA.0 + INIA 0d200 + SETD.0 MulB + STA.0 + CALL show ; 3 x 200 + + INIA 0d200 + SETD.0 MulA + STA.0 + INIA 0d3 + SETD.0 MulB + STA.0 + CALL show ; and 200 x 3, which had better agree. + + INIA 0xFF + CALL both + CALL show ; 255 x 255, the largest a byte times a byte can be. + + HALT + +; A in both operands, for the square cases. +both: + SETD.0 MulA + STA.0 + SETD.0 MulB + STA.0 + RET + +; A in both, then show it. The nought case wants this and nothing else does. +times: + CALL both + CALL show + RET + +; The product, high byte then low, which is how a sixteen bit number reads. +show: + CALL mul8 + SETD.0 MulHigh + LDA.0 + CALL printByteHex + SETD.0 MulLow + LDA.0 + CALL printByteHex + CALL lineFeed + RET diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index a597f08..094034c 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -125,7 +125,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 210 tests, of which 148 run, 35 +everything it printed against a file in `Tests/expected`. 211 tests, of which 149 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/mulTest.out b/Tests/expected/mulTest.out new file mode 100644 index 0000000..bdce856 --- /dev/null +++ b/Tests/expected/mulTest.out @@ -0,0 +1,9 @@ +0000 +0000 +0001 +0090 +0258 +0258 +FE01 +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index a097b0d..de11a40 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -73,6 +73,16 @@ tune | CosmOS/Source/cosmos.asm | run | tune.in # is the whole point of keeping both: the pair is a direct before and after. 16bitSegmentedSieveModern | Examples/primeSieve/16bitSieveModern.asm | run | - | - mathTest | testPrograms/mathTest.asm | run | - | - +# ---- Multiplying, on a machine with no multiplier ---- +# +# a * b = qs[a+b] - qs[|a-b|], where qs[n] is n squared over four: two lookups and a subtract. +# And the table of quarter squares is built by ADDING, because qs[n] = qs[n-1] + n/2 - so +# nothing in the arrangement needs a multiply in order to exist, which is the point of it. +# +# The cases are nought, the commutation both ways round, a square, and 255 times 255, which is +# the largest product two bytes hold. A square is the case the identity leans on hardest: the +# difference term is nought and the whole answer comes from one entry. +mulTest | testPrograms/mulTest.asm | run | - | - printTest | testPrograms/printTest.asm | run | - | - int16print | Libraries/int16print.asm | run | - | -