From d896c9d43384492e6ec93ef00aca7336c838d9ae Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Thu, 3 Sep 2026 18:07:07 -0400 Subject: [PATCH] Multiplying, on a machine with no multiplier a * b = qs[a + b] - qs[|a - b|] where qs[n] is n squared over four because (a+b)^2/4 minus (a-b)^2/4 is exactly a*b, and the halves the flooring throws away cancel between the two terms. A multiply is two lookups and a subtract. AND THE TABLE IS BUILT BY ADDING, which is the part that makes it fit a machine with no multiplier at all. A table of squares would need squaring to fill; this one does not, because qs[n] = qs[n-1] + n/2, and n/2 goes 0, 1, 1, 2, 2, 3 - a number that steps up on every even n. So the whole thing is a running total and a toggle, and nothing harder than an add appears anywhere in building the thing that does the multiplying. 511 entries of two bytes, because a byte plus a byte reaches 510. That is 1,022 bytes of Data Memory, and it is the price: a kilobyte traded for an operation the hardware has not got. The operands go in memory rather than in registers. B cannot be stored and a product does not fit in one byte anyway, so two in and two out would spend more instructions shuffling than the multiply costs. Checked against nought, the commutation both ways round, a square, and 255 times 255 - which is 0xFE01 and the largest product two bytes hold. The square is the case the identity leans on hardest: the difference term is nought and the whole answer comes out of one entry. Wanted for Lunar Porter's orbit, where the trade between height and speed has to be proportional to vx times vy and could not be. Useful well beyond it: this is the routine every fixed point sum on this machine has been doing without. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/Libraries/math.asm | 235 ++++++++++++++++++++++++++++++ Programs/testPrograms/mulTest.asm | 83 +++++++++++ SplitBit Test Manual.md | 2 +- Tests/expected/mulTest.out | 9 ++ Tests/manifest | 10 ++ 5 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 Programs/testPrograms/mulTest.asm create mode 100644 Tests/expected/mulTest.out 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 | - | -