19

Can anyone help me understand what is happening inside this algorithm scaling an 8-bit value to 16-bit?

PATCH_LOAD_QUANTISE_VALUE:
 ASLA
 LDAB    #165
 MUL
 ASLB
 ROLA
 RTS

https://github.com/ajxs/yamaha_dx7_rom_disassembly/blob/master/yamaha_dx7_rom_v1.8.asm#L7378

A while ago I completed a project reverse-engineering some 8-bit synthesizer firmware. One commonly used function in the firmware converted a value serialised in the range 0-99, to its internal 16-bit representation in the range 0-65535.

I don't have much experience with programming for retro systems, so I've never encountered this kind of algorithm before. I suspect there's some common method for deriving the magic number 165 in the subroutine.

Any help understanding what I'm looking at would be appreciated. Thank you!

1
  • 2
    65536/100=655. 655/4 (the value is shifted twice) ~165. Not a lot of magic in here.
    – tofro
    May 29, 2022 at 12:08

3 Answers 3

39

Technically, this is just scaling, as quantization means a completely different process.

That's simply a multiplication, done with an algorithm which exploits the range of used numbers and avoids the limitations of the HD6303 MCU to calculate result quickly with one MUL instruction and avoiding use of temporary RAM variables for intermediate results.

To scale range of 0..99 to 0..65535, you would need to multiply 99 by approximately 662.

The HD6303 can only multiply two 8-bit values together, from registers A and B, and the 16-bit output result will be in 8-bit registers A and B, collecively called the 16-bit register D.

So as 662 does not fit into 8-bit register, the operation must be split up. Generally how this would be done is to do it in software and use the MUL opcode several times to be able to perform a 16x8 multiplication and use memory for storing intermediate results. Sort of same thing when you do multi-digit multiplication with pen and paper.

So while this could be done by implementing a general subroutine for multiplication of two larger numbers, it would be somewhat slower.

By exploiting the range of input numbers and sacrificing precision only slightly, it can be done extemely quickly with a single MUL operation and without using any memory for intermediate storage so the values are kept in registers.

The original multiplier is 662. It won't fit into 8 bits. Dividing it by 4 gives 165 which does fit into 8 bits.

The original input value is up to 99. It fits into 8 bits.

So the 99 could be multiplied with 165, and the result would fit to 16 bit register D. However it would still need multiplication by 4, which would equal to two rounds of shift/rotate.

Pre-multiplying the 99 by 2 by shifting left once also fits into 8 bits and can be multiplied. Therefore the result in D only needs to be multiplied by 2 by one round of shift/rotate to get the correct range.

So in pseudocode, it's just

D=660*A

but calculated as

D = (((A*2) * 165) *2)

6
  • 2
    Thank you for your explanation! In retrospect this does seem much more apparent. In my defense, I guess sometimes you just can't see the forest for the trees when you're working backwards from someone else's written assembler.
    – ajxs
    May 29, 2022 at 22:12
  • As there are only 100 values, would a table lookup be faster? Jun 1, 2022 at 20:14
  • 1
    @ThorbjørnRavnAndersen No, lookup table would be much slower. Current subroutine that takes parameter in register A, returns value in D, and trashes no other registers, takes 17 CPU cycles. Lookup would need to push/pop index register X to preserve it before loading table offset to X (LDX #addr), and would have to transfer parameter from A register to B (TAB), as only register B can be added to X in a single opcode (ABX). A left shift (ASL) is also needed to index words instead of bytes. And it would require 200 bytes of ROM for the table, but the ROM is so full there is no space for it.
    – Justme
    Jun 1, 2022 at 21:24
  • @ThorbjørnRavnAndersen Oh and I believe the original subroutine could still be optimized down to 16 CPU cycles.
    – Justme
    Jun 1, 2022 at 21:30
  • @Justme Ah! I don't know this CPU so I was just wondering, because multiplications often are relatively slow. Thanks! Jun 2, 2022 at 7:43
15

The algorithm is called "multiplication".

The code appears to compute A×2×165×2, which simplifies to A×660. This linearly maps the range 0–99 into 0–65,340.

I don't know the 6303, but presumably the code does it this way because loading an 8-bit constant (165) and shifting twice is faster than loading a 16-bit constant (660).

1
  • 8
    Not only faster, it's the only way to multiply numbers in hardware and the instruction multiplies two 8 bit registers.
    – Justme
    May 29, 2022 at 8:54
12

If you look at what's happening step for step, you can see where the 165 is coming from:

  • A is multiplied by 2 (ASLA)
  • A*2 is now multipled by "magic" value 165 (LDAB #165, MUL)
  • Result (HI part in A, LO part in B) is multiplied by 2 as 16-bit quantity (ASLB, ROLA)

So if A is 99 (full scale), 99*2 = 198, 198*165 = 32670, 32670*2 = 65340 - in other words, the magic constant is just the scaling factor of 65340/99/4 = 165

The approach of multiplying (((A by 2) by 165) by 2) to achieve a multiplication by 660 is likely done as an architecture specific optimization and to be able to use MUL with 8-bit quantities (knowing A by 2 will always fit in 8 bits).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .