Post Reply 
Scientific RPN Calculator (with ATTINY85)
03-08-2018, 03:02 AM (This post was last modified: 03-08-2018 03:14 AM by sa-penguin.)
Post: #21
RE: Scientific RPN Calculator (with ATTINY85)
That _exp_sin code is... cool.

I notice you use standard Arduino code for log(x) and atan(x) functions.
I also noted temporary results held in multiple registers: SWAP, ROTUP, ROTDOWN and all the trig functions.

I don't know if the code would be smaller or faster if you had one set of common registers. If you wanted to keep names to make the code easier to read, I'd suggest a union structure (lets you call the same variable by multiple names).

I'd also suggest moving the square root code to the "sub programs" area, and calling it during the trig functions:

double _sqrt(double f) {
result = _exp_sin(0.5 * log(f), true);
return result;
}
...
case SQRT: // SQRT
x = _sqrt(x);
break;
...
double as = atan(x / _sqrt(1 - x * x));
...
else if (key == ASINH) x = log(x + _sqrt(tmp));
else if (key == ACOSH) x = log(x + _sqrt(tmp - 2));


That may make the code slightly smaller, but also improve readability.

Just my $0.02 worth
Find all posts by this user
Quote this message in a reply
03-08-2018, 11:05 AM
Post: #22
RE: Scientific RPN Calculator (with ATTINY85)
Hi all!

It is like christmas - the codesize is now 7368 bytes which means I have 824 bytes free for more functions!

I changed (see attached code file):
* Stack variables (xyzu) as global (incl. push and pull subroutines) -510 bytes
* Declare subroutines and some local variables as static -224 bytes
* Own subroutine _sqrt() which holds calculation formula -37 bytes
* Bug: Push stack if number entering starts with '.' +16 bytes
* Bug: 1/x for negative values +4 Bytes

Some hints did not free space:
* Own CASE for atan and atanh (+16 bytes)
* Replace log10() with log()/log(10) (+50 bytes)

For some hints I have further questions:
* @Pauli: I can not see how to merge asinh and acosh

Some hints mean spending more time:
* Avoiding some math functions
* Minimize buttoncast()

And there are topics where my knowledge is to weak:
* How can I reduce the optimization level in the arduino suite (IDE) to check if the code will become smaller?
* I would like to place data in the EEPROM (512 bytes) and read it with EEPROM.read. But when I flash the ATTINY85 (via Uno as ISP) the EEPROM of the ATTINY is deleted. I read something about setting fuse EESAVE. How can do this in the arduino suite (IDE)?

Next plans:
It is relatively easy to implement
* physical constants to EEPROM (but how to not burn EEPROM?)
* statistics/regression
* conversions: polar/complex h/h.ms
* functions: nCr nPr

What is your opinion what to implement first (as long as free memory exists)?

Regards
deetee


Attached File(s)
.txt  180308_scary.txt (Size: 25.11 KB / Downloads: 8)
Find all posts by this user
Quote this message in a reply
03-08-2018, 12:34 PM
Post: #23
RE: Scientific RPN Calculator (with ATTINY85)
A pretty good win so far Smile


(03-08-2018 11:05 AM)deetee Wrote:  * Own CASE for atan and atanh (+16 bytes)

This comes back to the inefficient switch implementation I suspect. These should both save space but don't. A table of function pointers indexed by the key codes is worth a try. Keep the key code in a global so the functions are reusable. That is, sin/cos and tan can all use the same function and return different values based on the key code used. This will require renumbering the key codes.


Quote:* Replace log10() with log()/log(10) (+50 bytes)

I found the source for the maths library, it uses log()*.43429... for this and avoids C's overheads. The is no chance on a saving from log10.


Quote:* @Pauli: I can not see how to merge asinh and acosh

Probably unnecessary with sqrt as a separate function. The idea was that the difference between the two was the sign: x2 + 1 or x2 - 1 and thus the duplicate equation could be avoided.


Quote:* I would like to place data in the EEPROM (512 bytes) and read it with EEPROM.read. But when I flash the ATTINY85 (via Uno as ISP) the EEPROM of the ATTINY is deleted. I read something about setting fuse EESAVE. How can do this in the arduino suite (IDE)?

You could keep the calculator state in EEPROM to allow more registers and add some user program space.

Could the firmware set the fuse? You might need one firmware to load the constants and set the fuse and a second for the calculator.

512 bytes of data is a lot, but it will be hard to program up front. Better might be to think of things a user might want to put there:
  • A command / result history?
  • Save and restore stack?
  • User defined constants?

As far as I'm aware, the only calculator with EEPROM was the Elektronika MK 52 and it was hard to use but could store registers and programs.


Quote:What is your opinion what to implement first (as long as free memory exists)?
  • I'd go for the gamma function or log gamma. Factorial is free given gamma. Combinations and permutations are quite cheap if log gamma is available (but not if only gamma is available due to overflow concerns). Rskey.org has a wide variety of implementations for calculators.
  • log(1+x) and ex-1 are useful in their own right and are present in the code already (well the former appears).
  • Stopwatch mode.
  • Printing via IR LED (using one of the remaining three pins on the ATTINY).

Implementing h/h.ms properly is difficult with decimal arithmetic and very difficult when using binary like you are.


BTW: all the variables declared static inside functions are globals and possibly could be moved out of the function to the global namespace which might make the layout nicer.


Pauli
Find all posts by this user
Quote this message in a reply
03-08-2018, 12:54 PM
Post: #24
RE: Scientific RPN Calculator (with ATTINY85)
I think there is another small saving to be had.

Change the assignment of floats/doubles to use the memmove function instead. It seems smaller.
I haven't checked but it might even be worth having a copy function:
Code:
static void copy(double *dest, const double *src) {
    memmove(dest, src, sizeof(double));
}

In this case it is also possible to use memcpy directly since memmove uses memcpy for one direction of copy so no wasted code.

Also, change the stack functions to:

Code:
static void push(void) { // Push stack
  memmove(&y, &x, sizeof(y) * 3);
}

static void pull_upper(void) { // Pull upper part of stack
  memmove(&y, &z, sizeof(y)*2);
}

void pull(void) { // Pull stack fully
    memmove(&x, &y, sizeof(x)*3);
}

These have to be memmove calls since the blocks overlap. It is probably also sensible to declare the stack as a structure or array for this to be completely safe. There shouldn't be an extra overhead for doing so.

Pauli
Find all posts by this user
Quote this message in a reply
03-09-2018, 07:51 AM
Post: #25
RE: Scientific RPN Calculator (with ATTINY85)
Hi Pauli!

Thanks for all of your ideas.

I tried to implement statistics and linear regression - which needed approx. 1000 bytes - to much - so I refused this for now.

Then I implemented converting polar/rectangular coordinates and h/hms which "costs" me all of my spare 800 bytes.

I know the gamma function, but neither I needed in before nor saw it on basic HP-calculators. But the gamma function seems to be worth implementing and doing this with Nemes' formula should "cost" approx. 200 bytes.

I did not know the memmove function but I will give it a try (it is on my todo-list).

But (overnight) I fell in love with your idea to save user defined constants to the EEPROM. I gave it a rough try and implemented
"saving 3 characters and a float number" to a "EEPROM slot" and
"menu driven selecting due to these 3 characters and loading the float number".
This demands approx. 700 bytes only (and there is still room for improvement).

Still a lot to do this weekend ...

Regards
deetee
Find all posts by this user
Quote this message in a reply
03-09-2018, 08:05 AM
Post: #26
RE: Scientific RPN Calculator (with ATTINY85)
Gamma first appeared on the HP-34C and I believe it is present on most subsequent models. It is disguised as x!. Models without call it n!. For small arguments, the usual approach is to make them large using the recurrence relationship. Negative arguments require the reflection formula but this could be left out.

Take care with the edge cases with h/h.ms, they can be tricky.


Pauli
Find all posts by this user
Quote this message in a reply
03-09-2018, 09:22 AM
Post: #27
RE: Scientific RPN Calculator (with ATTINY85)
Hello Pauli!

Wow I did not know that my HP35 (disguised as !) calculates gamma instead of faculty. Right now I have implemented the typical for-loop to calculate n!
Can you recommend a smaller formula to calculate gamma other than the Nemes approach (Stirling works for big numbers accurate only).

For the h/hms converting I developed these formulae (did not check "edge cases"):

Code:
case HMS2H: // H.MS->H
  lastx = x;
  x = (int)x + ((int)(100 * (x - (int)x))) / 60.0 + (100 * (100 * x - (int)(100 * x))) / 3600.0;
  break;
case H2HMS: // H->H.MS
  lastx = x;
  x = (int)x + ((int)(60 * (x - (int)x))) / 100.0 + (60 * ((60 * (x - (int)x)) - (int)(60 * (x - (int)x)))) / 10000.0;
  break;

By the way: Your memmove code confuses the calculator. Maybe because the stack variables are not "in a memory line" - I did not check to do it with a stack array.
But changing the assignments in push/pull each for each with memmove saves me approx. 30 bytes.
Changing other assignments with the help of your copy()-function seems to use more memory.

Code:
static void push(void) { // Push stack
  //u = z; z = y; y = x;
  //memmove(&y, &x, sizeof(y) * 3);
  memmove(&u, &z, sizeof(double));
  memmove(&z, &y, sizeof(double));
  memmove(&y, &x, sizeof(double));
}

Regards
deetee
Find all posts by this user
Quote this message in a reply
03-09-2018, 10:02 AM (This post was last modified: 03-09-2018 10:03 AM by Paul Dale.)
Post: #28
RE: Scientific RPN Calculator (with ATTINY85)
If the stack is put into a struct, they be contiguous:

Code:
static struct {
    double x;
    double y;
    double z;
    double u;
} stack;

Or use different names and some macros to avoid changing any other code.


Pauli
Find all posts by this user
Quote this message in a reply
03-09-2018, 10:15 AM
Post: #29
RE: Scientific RPN Calculator (with ATTINY85)
(03-09-2018 09:22 AM)deetee Wrote:  For the h/hms converting I developed these formulae (did not check "edge cases"):

This will do weird things converting to h.ms Sad
The fix is to decompose the number after conversion and check boundary conditions. For seconds, this would be:

Code:
if ((int)(x * 10000) % 100 == 60) x += 0.004;

Likewise for minutes. You also need to be careful of the sign by converting to the absolute value before making any checks and putting the sign back afterwards. Even then, I'm not sure binary arithmetic can even do this because none of the fractions can be represented exactly.


Pauli
Find all posts by this user
Quote this message in a reply
03-09-2018, 10:28 AM
Post: #30
RE: Scientific RPN Calculator (with ATTINY85)
(03-09-2018 09:22 AM)deetee Wrote:  Wow I did not know that my HP35 (disguised as !) calculates gamma instead of faculty. Right now I have implemented the typical for-loop to calculate n!
Can you recommend a smaller formula to calculate gamma other than the Nemes approach (Stirling works for big numbers accurate only).

Nemes is also for large numbers. Lanczos is another commonly used approximation. All are okay. The trick is to make sure the number is large enough:

Code:
double fac = 1;
while (x < too_small) fac *= x++;
...
return fac * result;

The required threshold isn't big but it depends on the approximation used.
You'll want to check the recurrence.


Pauli
Find all posts by this user
Quote this message in a reply
03-15-2018, 11:18 AM
Post: #31
RE: Scientific RPN Calculator (with ATTINY85)
Hi Pauli!

After some days of work I am proud to show you a new (much better version of ScArY) with a code size of 8130 bytes.

I did not finish testing and documentation - but maybe you can view the attached file for improvements, bugs and optimization?

I made this major changes so far:
* Using a stack array (X, Y, Z, T, STO) now which works well with your push/pull/memmove solution.
* Changed the switch/case Statement to an if/elseif structure.
* Saving the calculator state (stack, brightness) to EEPROM when activating the screensaver (keys: f-f).
* Save up to 41 user defined constants/numbers with up to three characters per number to EEPROM. Browsing and loading this saved constants with cursor keys or direct number input.
* "Type-Recorder" function: Record 4x51 keypresses to EEPROM and replay them via user menu (a, b, c, d).
* Changed keyboard layout to bring important functions (menu, const, sto) closer to the f-key.
* Abandoned ROTup for saving codesize and faster menu entering.
* Abandoned my loved annuity function to save codesize and to minimize the number of menu lines. I can "program" this function later with the type recorder function.
* Due to your recommendation I extended the faculty function to a gamma function (according to the Nemes formula).

There are still some bytes left and I am sure you will find some more and have some ideas how to use them.

Thanks in advance
deetee


Attached File(s)
.txt  180315_scary.txt (Size: 41.99 KB / Downloads: 7)
Find all posts by this user
Quote this message in a reply
03-15-2018, 12:19 PM
Post: #32
RE: Scientific RPN Calculator (with ATTINY85)
Great going! That's a lot of functionality cramed in.

I wonder if this saves any space:

Code:
      else if (key == SIN || key == COS || key == TAN) { // Trigonometric
        double si = _exp_sin(stack[0] / rad, false);
        double co = _sqrt(1 - si * si);
        if (key == SIN) stack[0] = si;
        else if (key == COS) stack[0] = co;
        else stack[0] = si / co;
      }
      else if (key == ASIN || key == ACOS) {
        double as = atan(stack[0] / _sqrt(1 - stack[0] * stack[0]));
        if (key == ASIN) stack[0] = as * rad;
        else stack[0] = (PI / 2 - as) * rad;
      } else if (key == ATAN)
        stack[0] = atan(stack[0]) * rad;
      else if (key == SINH || key == COSH || key == TANH) { // Hyperbolic
        double ep = _exp_sin(stack[0], true);
        double em = _exp_sin(-stack[0], true);
        if (key == SINH) stack[0] = (ep - em) / 2;
        else if (key == COSH) stack[0] = (ep + em) / 2;
        else stack[0] = (ep - em) / (ep + em);
      } else if (key == ASINH || key == ACOSH) {
        double tmp = stack[0] * stack[0] + 1;
        if (key == ASINH) stack[0] = log(stack[0] + _sqrt(tmp));
        else stack[0] = log(stack[0] + _sqrt(tmp - 2));
      } else if (key == ATANH)
        stack[0] = 0.5 * log((1 + stack[0]) / (1 - stack[0]));

Likewise, renumbering the functions (so they start from zero and are contiguous) and using an array of function pointers might help (it did on the 34S). This will depend on the processor.


This might also help:

Code:
      else if (key == SINH || key == COSH || key == TANH) { // Hyperbolic
        double ep = _exp_sin(stack[0], true);
        double em = _exp_sin(-stack[0], true);
        double s = (ep - em) / 2;
        double c = stack[0] = (ep + em) / 2;
        if (key == SINH) stack[0] = s;
        else if (key == COSH) stack[0] = c;
        else stack[0] = s / c;


Subject to typos and copy/paste errors.


Pauli
Find all posts by this user
Quote this message in a reply
03-15-2018, 01:24 PM
Post: #33
RE: Scientific RPN Calculator (with ATTINY85)
Hi Pauli!

Thanks for your time and fast reply.

I still have played a lot with this part of code - since it was one of your first ideas to "split off" the ATAN function.

But I tried your new code wich has a much higher readability.
Unfortunately the code splitting of the trig and hyp functions costs 44 extra bytes.
And optimizing the em/ep/s/c stuff cost 22 bytes extra.
:-(

I still read some articles about function pointer arrays but did not see an advantage because I still need the if/elseif structure to decide which function to call - and the code is now in a subroutine. ... or am I missing the clue?
I will read more about this ...

Regards
deetee
Find all posts by this user
Quote this message in a reply
03-15-2018, 01:31 PM
Post: #34
RE: Scientific RPN Calculator (with ATTINY85)
So if I understand it right, sometimes you go computing everything (say the result of 3 different trig functions) before knowing which function is needed, to save bytes? This because once you have the result you need less code here and there?

Wikis are great, Contribute :)
Find all posts by this user
Quote this message in a reply
03-15-2018, 01:47 PM
Post: #35
RE: Scientific RPN Calculator (with ATTINY85)
Hello pier4r!

In principle you are right.

It seems that the "big" if/elseif structure (I had a "hungry" switch/case before) is less "hungry" but still hungry.

So it is smaller to check in one ifelse some several functions (ie trigonometric) and decide (in a local if/ifelse structure) later which of all this options applies. One advantage of this behavior is to calculate some common calculations and save the results in local variables.
The readability suffers and it surely costs speed but it saves bytes ...

Regards
deetee
Find all posts by this user
Quote this message in a reply
03-15-2018, 10:10 PM
Post: #36
RE: Scientific RPN Calculator (with ATTINY85)
(03-15-2018 01:24 PM)deetee Wrote:  I still read some articles about function pointer arrays but did not see an advantage because I still need the if/elseif structure to decide which function to call - and the code is now in a subroutine. ... or am I missing the clue?

One way to do it would be as follows:

You have a table of functions:

Code:
static enum {
  ENTER=0, CLX, ..., SIN, COS, TAN,...
} key;

static void do_enter(void) {
      push();
      ispushed = true;
      isnewnumber = true;
      isee = false;
      isenteringnumber = false;
}

void do_clx(void) {
      stack[0] = 0.0;
      isee = false;
}

static void do_trig(void) {
        double si = _exp_sin(stack[0] / rad, false);
        double co = _sqrt(1 - si * si);
        double as = atan(stack[0] / _sqrt(1 - stack[0] * stack[0]));
        if (key == SIN) stack[0] = si;
        else if (key == ASIN) stack[0] = as * rad;
        else if (key == COS) stack[0] = co;
        else if (key == ACOS) stack[0] = (PI / 2 - as) * rad;
        else if (key == TAN) stack[0] = si / co;
        else stack[0] = atan(stack[0]) * rad;
 }

static void (*dispatch)(void)[] = {
  &do_enter, &do_clx , ..., &do_trig, &do_trig, &do_trig, ...;
};

Then to call...

Code:
key = ???;
(*dispatch[key])();


This avoids the if then else chains replacing them with an array look up. It will come down to the overhead of the function wrappers vs the conditional code. A drop to assembly would make this smaller because of the shortcuts that can be taken.


Pauli
Find all posts by this user
Quote this message in a reply
03-16-2018, 05:49 AM
Post: #37
RE: Scientific RPN Calculator (with ATTINY85)
Hello Pauli!

Thanks for your code and your "private lesson" - I think I see the advantage to avoid the "hungry" if/elseif loop.

This is a lot of work but I will give it a try ...

Regards
deetee
Find all posts by this user
Quote this message in a reply
03-18-2018, 10:39 AM
Post: #38
RE: Scientific RPN Calculator (with ATTINY85)
Hi Pauli!

First my compiler demanded
Code:
static void (*dispatch[])(void)
instead of
Code:
static void (*dispatch)(void)[]
but then it compiled.

I replaced the big if/ifelse-loop (from ADD to ATANH) with a function pointer array but unfortunately this costs me approximately 590 bytes more. So it seems that the function pointer array is even "more hungry" than the switch/case solution.

After remaining with the if/ifelse-loop and some bug repairs after testing I ended with 8168 bytes of code. That is really amazing with this much more functionality than version 1.

So far I added the gamma function (250 bytes), the saving state solution, the saving constants solution and finally the amazing "Type Recorder" programming function.
I kept the Gauss PDF and CDF function (the latter is rather unique for pocket calculators).
I abandoned the ROTup (= 3x ROTdown) and annuity function (can be recorded with the Type Recorder) and I did not implement conversion functions (can be recorded too - like the P->R and R->P functions - or can be solved with the help of saved conversion constants).

I think version 2 is ready now. Right now I uploaded a video to youtube and I have to renew the documentation at github and publish a post at hpmuseum next week.

Thanks for facilitating this.
deetee
Find all posts by this user
Quote this message in a reply
03-18-2018, 10:49 AM
Post: #39
RE: Scientific RPN Calculator (with ATTINY85)
That's unfortunate. There might be some possibilities ... make the function table one if the distance between functions rather than pointers (hoping to get this down to a single byte), find a way to make the called functions shorter by disabling the C call conventions. Both are probably hopeful, in that I doubt they'd save enough to be worthwhile.

At least there is a lot more there than before!


Pauli
Find all posts by this user
Quote this message in a reply
03-20-2018, 01:01 PM
Post: #40
RE: Scientific RPN Calculator (with ATTINY85)
Hello Pauli!

You know that I am not very fast - but finally I took up your hint that the casting of the keycodes to ascii-characters can be done smarter:

(03-08-2018 01:43 AM)Paul Dale Wrote:  * I think buttoncast can be done with the tables by renumbering the KC use 0x00 - 0x0F and some bit operations. A straight log2 of the key scan could be the key code but changes elsewhere (checking for digits using ranges) would need modification. There might be a smarter way.

And you are right. I tried this faster and shorter code with your log2-idea:

Code:
static byte buttoncode() { // Returns byte value with key number
  unsigned int c = getbuttons(); //  1  3  5  7
  if (c & 0x1) return (16);      //  9 11 13 15
  byte ans = NULL;               // 16  2  4  6
  while (c >>= 1) ans++;         //  8 10 12 14
  return (ans);
}

static byte buttoncast() { // Get button pattern and return keycode
  //           key number:      1   10    2   11    3   12    4   13    5   14    6   15    7   16    8    9
  // code of buttoncode():  0   1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16
  byte keycast[KEYS + 1] = {0, 'f', '1', '7', '2', '8', '3', '9', 'c', 'E', '0', '4', '.', '5', ' ', '6', '#'};
  return (keycast[buttoncode()]);
}

And it saves me 40 (!) bytes more - great.

Mayby there are more "gold nuggets" in your former posts ...

Thank you again.
deetee
Find all posts by this user
Quote this message in a reply
Post Reply 




User(s) browsing this thread: 1 Guest(s)