update readme again

This commit is contained in:
Jeff Wang 2019-10-07 03:51:28 -04:00
parent b18969fef0
commit 73585be8bb

View File

@ -2,22 +2,25 @@
This is a replacement firmware for the [diyleyuan calculator kit](http://www.diyleyuan.com/jc/L8Q.html). The calculator kit is available for purchase for less than $13 shipped from eBay by searching for "diy calculator kit". You will have to solder the kit yourself (see "hardware connections" below). The calculator uses an STC IAP15W413AS microcontroller (an 8051 instruction set-compatible microcontroller) with a built-in serial-port bootloader. This project uses SDCC to compile the C code and [stcgal](https://github.com/grigorig/stcgal) to load the new firmware. This is a replacement firmware for the [diyleyuan calculator kit](http://www.diyleyuan.com/jc/L8Q.html). The calculator kit is available for purchase for less than $13 shipped from eBay by searching for "diy calculator kit". You will have to solder the kit yourself (see "hardware connections" below). The calculator uses an STC IAP15W413AS microcontroller (an 8051 instruction set-compatible microcontroller) with a built-in serial-port bootloader. This project uses SDCC to compile the C code and [stcgal](https://github.com/grigorig/stcgal) to load the new firmware.
The replacement firmware supports floating-point calculations, with a 4-level RPN stack. Functions include basic arithmetic as well as log(), exp(), y^x, 1/x and sqrt(), all in floating point. (The original firmware supported only fixed-point calculations in chain mode.) I have not added in the resistor value calculator or the decimal/hexadecimal converter features from the original firmware. The replacement firmware supports floating-point calculations (using 18 decimal digits plus exponent for arithmetic) with a 4-level RPN stack. Functions include basic arithmetic as well as log(), exp(), y^x, 1/x and sqrt(), all in floating point. (The original firmware supported only fixed-point calculations in chain mode.) I have not added in the resistor value calculator or the decimal/hexadecimal converter features from the original firmware.
Note that once you change the firmware on the calculator, there's no way to go back to the original firmware (the original firmware isn't posted for download anywhere). It also isn't possible to read back the code from an existing STC microcontroller. This is a deliberate "feature" of the STC bootloader to prevent readbacks. Note that once you change the firmware on the calculator, there's no way to go back to the original firmware (the original firmware isn't posted for download anywhere). STC's bootloader on the microcontroller deliberately prevents readback of the microcontroller's content, and STC considers this to be a "feature".
Here's a picture of the assembled calculator kit running the new firmware (it's impossible to keep the glossy black acrylic clean): Here's a picture of the assembled calculator kit running the new firmware (it's impossible to keep the glossy black acrylic clean):
![calculator](./calc.jpg) ![calculator](./calc.jpg)
And here's a picture of the GUI emulator with functions labeled:
![Qt GUI](./qt_gui.png)
# Building # Building
- The Makefile can be used for building a new firmware for the calculator. - Use the Makefile for building a new firmware for the calculator.
- type `make` to build - type `make` to build
- (you must already have SDCC installed and set up so it can be found in your PATH) - (you must already have SDCC installed and set up so it can be found in your PATH)
- this will create a `main.hex` output file - this will create a `main.hex` output file
- there is also a prebuilt binary checked in at `binaries/main.hex` - there is also a prebuilt binary checked in at `binaries/main.hex`
- CMakeLists.txt is for building the Qt desktop application, and also the decimal number library test application. - CMakeLists.txt is for building the Qt desktop application, and also the decimal-number-library test application.
- (still a work in progress)
- build similarly to other cmake projects: - build similarly to other cmake projects:
- `mkdir build_qt && cd build_qt` - `mkdir build_qt && cd build_qt`
- `cmake -DCMAKE_BUILD_TYPE=Debug -G "Eclipse CDT4 - Ninja" ..` - `cmake -DCMAKE_BUILD_TYPE=Debug -G "Eclipse CDT4 - Ninja" ..`
@ -29,7 +32,7 @@ Note that once you change the firmware on the calculator, it isn't possible to g
1. Buy a USB to logic-level serial dongle that supports 5V operation (these dongles may have a jumper you need to set to switch between 3.3V and 5V). This is the best option. 1. Buy a USB to logic-level serial dongle that supports 5V operation (these dongles may have a jumper you need to set to switch between 3.3V and 5V). This is the best option.
- Here is one that works: https://www.amazon.com/gp/product/B00N4MCS1A/ - Here is one that works: https://www.amazon.com/gp/product/B00N4MCS1A/
1. If you have an adjustable power supply, power the diyleyuan calculator at 3.3-3.5V for programming, instead of 5V, and use a 3.3V USB to logic level serial dongle. 1. If you have an adjustable power supply, power the diyleyuan calculator at 3.3-3.5V for programming, instead of 5V, and use a 3.3V USB-to-logic-level-serial dongle.
- You won't be able to see the LCD screen at 3.3V well, though. - You won't be able to see the LCD screen at 3.3V well, though.
- After programming, remove the USB dongle, and set the voltage back up to 5V to make the LCD readable. - After programming, remove the USB dongle, and set the voltage back up to 5V to make the LCD readable.
- This is an ok option, but is cumbersome to switch between 3.3V and 5V, and requires that you have an adjustable power supply. - This is an ok option, but is cumbersome to switch between 3.3V and 5V, and requires that you have an adjustable power supply.
@ -108,7 +111,7 @@ Disconnected!
# Usage # Usage
## Calculation ## Calculation
The calculator uses RPN. To calculate (2+3)/(9^2), enter: The calculator uses RPN. Calculate (2+3)/(9^2) by pressing the following keys:
- 2 - 2
- Enter (=) - Enter (=)
@ -126,7 +129,7 @@ Some of the keys have slightly different functions, see the picture of the emula
![Qt GUI](./qt_gui.png) ![Qt GUI](./qt_gui.png)
The keys on the original calculator map as follows: The keys on the *original* calculator map as follows:
- `= `: Enter - `= `: Enter
- `<- `: Negate (+/-: change sign) - `<- `: Negate (+/-: change sign)
@ -153,11 +156,11 @@ The keys on the original calculator map as follows:
The exponent display is fixed at 2 digits, although the calculator doesn't prevent you from doing certain operations (e.g. basic arithmetic) which results in numbers with larger exponents. (A 10 in the ten's place of the exponent will then be displayed as a '`:`'. This just so happens to be the next character after '`9`' in the 1602 LCD's character map). The calculator can internally represent larger exponents than +/-99, although calculations with larger exponents aren't guaranteed to be correct. The exponent display is fixed at 2 digits, although the calculator doesn't prevent you from doing certain operations (e.g. basic arithmetic) which results in numbers with larger exponents. (A 10 in the ten's place of the exponent will then be displayed as a '`:`'. This just so happens to be the next character after '`9`' in the 1602 LCD's character map). The calculator can internally represent larger exponents than +/-99, although calculations with larger exponents aren't guaranteed to be correct.
## Turning off ## Turning off
Hold `Shift` (the `mode` key on the physical calculator) and `0` at the same time to turn off. NOTE: There is no auto power off. Hold `Shift` (the `mode` key on the physical calculator) and `0` *at the same time* to turn off. NOTE: There is no auto power off.
# Bugs # Bugs
1. The calculator does not properly check for underflow or overflow sometimes. 1. The calculator does not properly check for underflow or overflow sometimes.
1. After division by 0, ln(-), over/underflow, or other operations which give an `Error`, it's possible to still do certain operations on `Error`. Many functions do check, and will not operate on `Error`, but not all of them yet. This is somewhat similar to old soviet calculators where `Error` is just a number, and they didn't have enough rom space to check for errors, and there are people who explore the inner-workings of the calculator by manipulating the `Error` number. 1. After division by 0, ln(-), over/underflow, or other operations which give an `Error`, it's possible to still do certain operations on `Error`. Many functions do check, and will not operate on `Error`, but not all of them yet. This is somewhat similar to old soviet Elektronika calculators where `Error` is just a number, and there wasn't enough ROM space to check for errors. There are people who explore the inner-workings of these calculators by manipulating the `Error` number.
1. There are probably more bugs waiting to be discovered. 1. There are probably more bugs waiting to be discovered.
# Internals # Internals
@ -166,11 +169,12 @@ The original firmware that came with this calculator used a fixed-point format,
This replacement calculator firmware uses decimal floating point, using base-100 to store numbers and do calculations. Base-100 allows for efficient storage into 8-bit bytes, and is easier to work with than packed-BCD. Unlike straight binary representations, base-100 is still fairly easy to display as decimal. Also unlike binary representations, there is no conversion error from binary/decimal (e.g. numbers like `0.1` can be represented exactly). This replacement calculator firmware uses decimal floating point, using base-100 to store numbers and do calculations. Base-100 allows for efficient storage into 8-bit bytes, and is easier to work with than packed-BCD. Unlike straight binary representations, base-100 is still fairly easy to display as decimal. Also unlike binary representations, there is no conversion error from binary/decimal (e.g. numbers like `0.1` can be represented exactly).
Each `uint8_t` stores a base-100 "`digit100`", referred to as an "`lsu`", for least significant unit (the terminology is borrowed from the decNumber library: I originally considered using the decNumber library similar to the WP-34S calculator, but just the library itself takes several times more flash than is available on this calculator). The number format is as follows: Each `uint8_t` stores a base-100 "`digit100`", referred to as an "`lsu`", for least significant unit. (The LSU terminology is borrowed from the decNumber library: I originally considered using the decNumber library similar to the WP-34S calculator, but just the library itself takes several times more flash than is available on this calculator. I also considered using the BigNu mber arduino library, but that library uses C++ and lots of pointers passed to functions, which are extremely expensive on the 8051 architecture.) The number format is as follows:
- `lsu[0]`: contains the most significant `digit100` (the most significant 2 decimal digits) - `lsu[0]`: contains the most significant `digit100` (the most significant 2 decimal digits)
- implicit decimal point between `lsu[0]/10` and `lsu[0]%10` - implicit decimal point between `lsu[0]/10` and `lsu[0]%10`
- `lsu[1]` to `lsu[n-1]`: the rest of the array contains the next `digit100`s in order from most to least significant - `lsu[1]` to `lsu[n-1]`: the rest of the array contains the next `digit100`s in order from most to least significant
- currently there are 9 LSUs, giving an 18 decimal digit significand
- exponent: the bottom 15 bits hold the exponent of the floating point number, the most-significant bit holds the sign - exponent: the bottom 15 bits hold the exponent of the floating point number, the most-significant bit holds the sign
- the exponent (the bottom 15 bits) is stored directly in 2's complement binary - the exponent (the bottom 15 bits) is stored directly in 2's complement binary
- this is a base-10 exponent, not a base-100 exponent - this is a base-10 exponent, not a base-100 exponent
@ -183,7 +187,7 @@ For example, the number `13.5` is stored normalized (with no leading zeros in th
- `lsu[0]`: 13 - `lsu[0]`: 13
- `lsu[1]`: 50 - `lsu[1]`: 50
- `lsu[2]` to `lsu[n-1]`: all zero - `lsu[2]` to `lsu[n-1]`: all 0
- exponent: 1 - exponent: 1
There is an implicit decimal point between the 1 and 3 in `lsu[0]`, so the number is 1.350 * 10^1, which is equivalent to `13.5`. Similarly, the number `1.35` would be stored the exact same way, except now the exponent is 0. There is an implicit decimal point between the 1 and 3 in `lsu[0]`, so the number is 1.350 * 10^1, which is equivalent to `13.5`. Similarly, the number `1.35` would be stored the exact same way, except now the exponent is 0.
@ -192,14 +196,14 @@ The number `0.135` would be stored the same way, except now the exponent is `0x7
## Arithmetic ## Arithmetic
- Addition is done the same way as it's done by hand, although in base-100 instead of decimal. - Addition is done the same way as it's done by hand, although in base-100 instead of decimal.
- Subtraction is similarly done similar to how it's done by hand, although with carries instead of borrows using the "equal additions" algorithm, and also in base-100. - Subtraction is similarly done similar to how it's done by hand, also in base-100. It uses carries instead of (the more widely taught) borrows using the "equal additions" algorithm.
- Multiplication is done the same way it's done by hand, although in base-100. Partial sums are added up and shifted after each digit, instead of waiting until the very end to sum up all partial sums (as is common when doing multiplication by hand). - Multiplication is done the same way it's done by hand, although in base-100. Partial sums are added up and shifted after each digit, instead of waiting until the very end to sum up all partial sums (as is common when doing multiplication by hand).
- Reciprocals are calculated using a fixed number of Newton-Raphson iterations, and division is calculated by taking the reciprocal of the divisor and multiplying by the dividend. - Reciprocals are calculated using a fixed number of Newton-Raphson iterations, and division is calculated by taking the reciprocal of the divisor and multiplying by the dividend.
- the iteration for 1/x is new_estimate = estimate + (1 - estimate * x)*estimate - the iteration for 1/x is new_estimate = estimate + (1 - estimate * x)*estimate
- see `src/decn/proto/div_mfp.cpp` for initial prototyping development work - see `src/decn/proto/div_mfp.cpp` for initial prototyping development work
## Transcendental Functions ## Transcendental Functions
- Logarithms are calculated similarly to described by the HP Journal article "Personal Calculator Algorithms IV: Logarithmic Functions" by William Egbert. - Logarithms are calculated similar to how it's described by the HP Journal article "Personal Calculator Algorithms IV: Logarithmic Functions" by William Egbert.
- see `src/decn/proto/ln_mfp.cpp` for initial prototyping development work - see `src/decn/proto/ln_mfp.cpp` for initial prototyping development work
- Exponentials are calculated similar to the HP 35 algorithm, as described [here](http://www.jacques-laporte.org/expx.htm) using the same constants as the logarithm algorithm. - Exponentials are calculated similar to the HP 35 algorithm, as described [here](http://www.jacques-laporte.org/expx.htm) using the same constants as the logarithm algorithm.
- see `src/decn/proto/exp.cpp` for initial prototyping development work - see `src/decn/proto/exp.cpp` for initial prototyping development work
@ -208,11 +212,11 @@ The number `0.135` would be stored the same way, except now the exponent is `0x7
## TODO ## TODO
- Rounding: currently, to save code space, there is no rounding being done (even for intermediate steps), and numbers are instead truncated. - Rounding: currently, to save code space, there is no rounding being done (even for intermediate steps), and numbers are instead truncated.
- Square roots could be more-accurately implemented using digit-by-digits methods similar to those described in the HP Journal article "Personal Calculator Algorithms I: Square Roots" by William Egbert. - Square roots could be more-accurately implemented using digit-by-digit methods similar to those described in the HP Journal article "Personal Calculator Algorithms I: Square Roots" by William Egbert.
- calculating using Newton-Raphson iterations for the reciprocal square root 1/sqrt(x), and then multiplying by the original value would probably also be more accurate, and definitely much faster - calculating using Newton-Raphson iterations for the reciprocal square root 1/sqrt(x), and then multiplying by the original value would probably also be more accurate, and definitely much faster
- the iteration for the reciprocal square root is new_estimate = 0.5 * estimate * (3 - x * estimate * estimate) - the iteration for the reciprocal square root is new_estimate = 0.5 * estimate * (3 - x * estimate * estimate)
- Reciprocal/division could also be more-accurately implemented digit-by-digit methods (the Newton-Raphson iterations are quite fast though). - Reciprocal/division could also be more-accurately implemented using digit-by-digit methods (the Newton-Raphson iterations currently used are quite fast though).
- Trigonometric functions could be implemented with algorithms similar to those described in the HP Journal articles "Personal Calculator Algorithms II: Trigonometric Functions" and "Personal Calculator Algorithms III: Inverse Trigonometric Functions" by William Egbert. - Trigonometric functions could be implemented with algorithms similar to those described in the HP Journal articles "Personal Calculator Algorithms II: Trigonometric Functions" and "Personal Calculator Algorithms III: Inverse Trigonometric Functions", both by William Egbert.
- will probably assign to the shifted `1`, `2`, and `3` keys, and `0` for calculating inverse trig functions. - will probably assign to the shifted `1`, `2`, and `3` keys, and `0` for calculating inverse trig functions.
- The stack rotate function is currently unimplemented - The stack rotate function is currently unimplemented
- will probably assign to the shifted `4` key - will probably assign to the shifted `4` key
@ -232,21 +236,21 @@ The keyboard matrix is scanned once every 5ms. The keyboard debouncing is based
In practice, the keyboard debouncing works much better than the original firmware (which would occasionally miss keystrokes). In practice, the keyboard debouncing works much better than the original firmware (which would occasionally miss keystrokes).
# Implementation on an STC 8051 Microcontroller # Implementation on an STC 8051 Microcontroller
This was my 1st time using an 8051 microcontroller. The architecture is a bit limiting for programming in C compared to more modern architectures -- even compared to other 8-bit microcontrollers such as the AVR (used in the arduino). Most significantly, there is no stack-pointer-relative addressing, which makes C functions takes up a lot of code space, since they must emulate stack-pointer-relative addressing. Unfortunately, the microcontroller used only has 13K of code space. The compiler used (SDCC) also does not support using a 2nd data pointer, even though STC's implementation of the 8051 has one. This was my 1st time using an 8051 microcontroller. The architecture is a bit limiting for programming in "high-level" languages such as C compared to more modern architectures -- even compared to other 8-bit architectures such as the AVR (used in the arduino). Most significantly, there is no stack-pointer-relative addressing, which makes C functions takes up a lot of code space, since they have to emulate stack-pointer-relative addressing. Unfortunately, the microcontroller used only has 13K of code space. The compiler used (SDCC) also does not support using a 2nd data pointer, even though STC's implementation of the 8051 has one.
I've avoided relying on the functions being able to be re-entrant, so that they do not depend on having a stack. SDCC is *not* set to use `--stack-auto` to reduce code size (this means functions are not re-entrant). Some "large" local variables are declared as static in functions to save on the code space needed to emulate a stack. There are a lot more globals than I would typically like to have, and less passing pointers to functions, since these are extremely expensive (to account for the 3 different memory types). I've avoided relying on the functions being able to be re-entrant, so that they do not depend on having a stack. SDCC is *not* set to use `--stack-auto` to reduce code size (this means functions are not re-entrant). Some "large" local variables are declared as static in functions to save on the code space needed to emulate a stack. I used a lot more globals than I what I would typically like to have used, and a lot less pointers passed to functions, since these are extremely expensive (to account for the 3 different memory types).
Another weird thing about the 8051 is that not all of the memory is addressed the same way. On this microcontroller, there are 512 bytes of ram total, of which: Another weird thing about the 8051 is that not all of the memory is addressed the same way. On this microcontroller, there are 512 bytes of ram total, of which:
- only 128 bytes can be addressed directly (or indirectly) - only 128 bytes can be addressed directly (or indirectly)
- the start of this address space is also shared with general purpose registers, so you don't actually have the full 128 bytes - the start of this address space is also shared with general purpose registers, so you don't actually have the full 128 bytes of directly addressable memory
- (there are also 128 bytes of "special function registers" which can be addressed directly to set up the microcontroller and its peripherals) - (there are also 128 bytes of "special function registers" which can be addressed directly to set up the microcontroller and its peripherals)
- 128 bytes can be addressed only indirectly (used in this firmware for the stack, and intermediate working registers) - 128 bytes can be addressed only indirectly (used in this firmware for the stack, and intermediate working registers)
- 256 bytes can be addressed as external memory - 256 bytes can be addressed as external memory
- on the original 8051, this memory would have actually been external, but on this microcontroller the "external" ram is built in - on the original 8051, this memory would have actually been external, but on this microcontroller the "external" ram is built in
- addressing this memory is more difficult than addressing indirect memory, which is itself more difficult than addressing memory directly - addressing this memory is more difficult than addressing indirect memory, which is itself more difficult than addressing memory directly
Thus, there are special compiler directives to tell it where to place things in memory. (Even for a simple calculator, there isn't enough directly addressable memory (128 bytes) to store everything.) General-purpose pointers and operations using general-purpose pointers are relatively expensive since the pointers must encode the memory type. Thus, there are special compiler directives to tell it what address space to place variables in memory. (Even for a simple calculator, there isn't enough directly addressable memory (128 bytes) to store everything.) General-purpose pointers and operations using general-purpose pointers are relatively expensive since the pointers must encode the memory type.
# Licensing # Licensing