How to optimize this code for Encode float to int and Decode int to Float

I am trying to build an algorithm for compressing 4 byte floats into a custom 2 byte uint16_t. I plan on sending a lot of data through the serial line from one Arduino to Another via I2c. I thought that if I "compress" the float into a uint16_t and have all the necessary information in the uint16_t to decode on the other end of the serial line, then I2c communications will be faster. This is for a button box for Microsoft Flight Simulator 2020. The numbers I need to send fall into certain categories:
1. Com/Nav Frequencies are positive and below 255 and have fractional components
-> 108.45, 112.95
2. Vertical Speed can be positive or negative and have no fractional component
-> -2000, 5000
3. Transponder codes need to be accurate and are between 0 and 7777
4. Anything above 255 does not have fractional component.
5. Anything value above 7777 does not need to be precise but better precision is preferred.
--> Doesn't matter if altitude is 40000 or 40050.

I think I have the code to satisfy the above requirements.

Can you guys look at this Encode and Decode snippet and see if there are any optimizations that could be done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

uint16_t Encode(float fnumber) {
  uint16_t inumber=abs(fnumber);
  //Lose precision, always positive -> store as square root -> first bit will be 1 and next 7 bits approximate the nearest 1/27 value using numbers 100-127
  if ((inumber > 16384) && (fnumber > 0)) { 
         float sroot=sqrt(fnumber); 
         //Extract whole number and set the first byte to its value
         inumber=(uint8_t) sroot;
         //Extract fractinal number set the second byte to its value
         //First Method, not very accurate but faster
         uint16_t ifract = (uint16_t) ((((sroot * 100) - (inumber*100)))/27)+100;  //Extract the fractional part as an int and divice by 27 and add to 100
         //second Method, more steps but better accuracy -->takes an extra 20 microseconds to finnish
         //uint16_t ifract = (uint16_t)  (round(((sroot * 100) - (inumber*100))/27))+100;
         
         inumber <<= 8;
         ifract <<=  1;
         inumber +=  ifract;
         inumber |= 1UL << 0;  //Set the first bit
        
         return inumber;
  // Precise and with a sign if no fractional component -> store as 14 bit number with first bit 0 and second bit sign with 0 positive and 1 negative      
  } else if ((inumber <= 16384 ) && ((int)fnumber == fnumber )) {
        
        inumber <<=  2;         //Shift 2 bits to the left, first bit is automatically zero
        //Extract the sign of the float and put it in the second bit
        
        if ((int)fnumber <0) {
            inumber |= 1UL << 1;
        }     
        return inumber;    
  // Precise and with a fractional component, always positive      
  } else if ((inumber < 256 ) && (fnumber > inumber)) {  
      //The whole number is already inumber
      //Extract the fractional number
      
       uint16_t ifract=(uint16_t)  ((fnumber * 100)-(inumber*100));
       inumber <<= 8;
       if (ifract > 0) {
         ifract <<=  1;
         inumber +=  ifract;
       }  
       inumber |= 1UL << 0;  //Set the first bit
       
       return inumber;
   } else  { //Error. Unhandled case
         return 0; //error condition
   }   
}


float Decode(uint16_t inumber) {
   //check first bit
     uint8_t bit = inumber & 1U; 
   if (bit) { // whole + fract
     //Extract whole number
     uint16_t whole= inumber >> 8;
     
     //Extract the fraction
     uint16_t fract = inumber & 0xFF;
    
     fract >>= 1; 
     if (fract > 99) {
        //Round away the inaccuracy and report in thousands only
        return (round((sq(whole + ((fract - 100 ) * 27)/(float)100))/1000))*1000;
        //Don't round away the inaccuracy
        //return sq(whole + ((fract - 100 ) * 27)/(float)100);
     } else {
       whole *=  100;
       return ((float)whole + fract)/100;
     }  
   } else {
     //Extract the sign from second bit
     uint8_t sign = inumber & 0x2;
     if (inumber & 0x2)
        return ((float)(inumber >> 2))*-1;
     else
        return (float)(inumber >> 2);
           
   }
}  



Testing the code yields:

Encode 65000.00 ==> 65231 ==> 65000.00 Encode time: 108 Decode time 112
Encode 44000.00 ==> 53709 ==> 44000.00 Encode time: 116 Decode time 112
Encode 44501.00 ==> 53967 ==> 44000.00 Encode time: 108 Decode time 112
Encode 15101.00 ==> 60404 ==> 15101.00 Encode time: 24 Decode time 8
Encode -5000.00 ==> 20002 ==> -5000.00 Encode time: 24 Decode time 8
Encode 250.00 ==> 1000 ==> 250.00 Encode time: 20 Decode time 8
Encode -25.00 ==> 102 ==> -25.00 Encode time: 24 Decode time 12
Encode -25.12 ==> 0 ==> 0.00 Encode time: 32 Decode time 4
Encode 108.45 ==> 27739 ==> 108.45 Encode time: 56 Decode time 44
Encode -100.21 ==> 0 ==> 0.00 Encode time: 28 Decode time 4
Encode -32000.00 ==> 0 ==> 0.00 Encode time: 12 Decode time 4


Times measured with micros().

If the code fails, the worst thing that could happen is my airplane crashes. But I can reload the flight :)

Thanks,
Chris
Last edited on
You're doing quite a bit of work per float that you're sending down the wire. Are you absolutely sure that pushing 16 bits through the serial connection is not faster than that function?
Not absolutely sure. But I also want values sent through the i2c bus to be uniform. If I send floats and strings and ints, they all show up as bytes since the Wire library can only read one byte at a time. I will have to reorganize each constituent byte to the proper data type and must send additonal information down the bus to identify the data type. I think that will take extra steps as well and I have to worry about byte ordering of each data type. Plus I want to be able to read ( on the sending side ) and write ( on the receiving side) directly to contiguous blocks of uint16_t memory, byte by byte as the Wire library reads or writes each byte.

This could be just a limitation of my knowledge of the arduino I2c, but at present I don't know a way that I could distinguish wether a byte coming in is part of an int, a float or string on the other end, unless I send additional information down the pipe to indicate the type.

Here is my question regarding the byte reading and writing technique that I am using. It seems to work well. It's almost like memcpy across the i2c bus.

https://www.cplusplus.com/forum/general/277405/
Last edited on
Why would you need to know the type of the serialized data? If you always serialize the information in the same order, as long both as the writer and the reader agree on that order then there's no need to send any information other than the data itself.
For example,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SomeThing{
    std::string name;
    float value;
    int quantity;
    
public:
    std::string write() const{
        std::string ret;
        write_string(ret, this->name);
        write_float(ret, this->value);
        write_int(ret, this->quantity);
        return ret;
    }
    static SomeThing read(const std::string &input){
        SomeThing ret;
        size_t offset;
        ret.name = read_string(input, offset);
        ret.value = read_float(input, offset);
        ret.quantity = read_int(input, offset);
        return ret;
    }
};

If the order can sometimes be different then just the type is not enough. You also need some way to identify which variable the next value is encoding.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SomeThing{
    std::string name;
    float value;
    int quantity;
    
public:
    std::string write() const{
        std::string ret;
        write_string(ret, "name");
        write_string(ret, this->name);
        write_string(ret, "value");
        write_float(ret, this->value);
        write_string(ret, "quantity");
        write_int(ret, this->quantity);
        return ret;
    }
    static SomeThing read(const std::string &input){
        SomeThing ret;
        size_t offset;
        while (offset < input.size()){
            std::string key = read_string(input, offset);
            if (key == "name"){
                ret.name = read_string(input, offset);
            }else if (key == "value"){
                ret.value = read_float(input, offset);
            }else if (key == "quantity"){
                ret.quantity = read_int(input, offset);
            }else
                throw std::runtime_error("can't decode");
        }
        return ret;
    }
};
Last edited on
A lot of data needs to be sent through the I2c bus which is why I chose the smallest discrete possible data type that is large enough to carry meaningful data. There would be multiple slaves attached to the bus and one master. Master reads data from an external source (PC), packages it for transit to the slaves. That's primarily why I chose the technique as I thought I have more processor than I have throughput in the bus. There might even be a possibility for the master arduino to just transit the package along and have the PC perform the actual packing. Multiple slave arduino's should have no problem decoding their own data packages. There is also a 32 byte limit to the I2c buffer and so I wanted to keep all the data compact and fit this limitation. I wanted a single write to transfer all the data that the slave needs for that cycle. By my estimate, that would allow me to transfer perhaps 16 boolean values ( packed in one uint16_t ), another uint16_t (packing list) to keep track (by setting each bit) which variables will be following the initial two bytes so a total of 14 numerical variables in 2 bytes each following the boolean and the packing list. The slaves also decide which variables they care about as pressing a button would change the panel display from Altitude to Vertical speed for example. So there is data traveling in the other direction (order list). In addition, the master might issue one byte transfers corresponding to buttons pressed on the master which the slaves need to receive and process into specific actions. So slaves are differentiating between a "command" (one byte) and a data package ("even number of bytes) by the size of the transfers alone. The pack and order list are important because I didn't want to just needlessly poll all the variables for all the slaves as that would saturate the serial line between the master and the PC. So the package size will shrink and grow according to what's needed at the moment.
Topic archived. No new replies allowed.