Add an example of handling doubles on AVR platform.
authorPetteri Aimonen <jpa@git.mail.kapsi.fi>
Tue, 29 Jan 2013 20:10:37 +0000 (22:10 +0200)
committerPetteri Aimonen <jpa@git.mail.kapsi.fi>
Tue, 29 Jan 2013 20:10:37 +0000 (22:10 +0200)
example_avr_double/Makefile [new file with mode: 0644]
example_avr_double/README.txt [new file with mode: 0644]
example_avr_double/decode_double.c [new file with mode: 0644]
example_avr_double/double_conversion.c [new file with mode: 0644]
example_avr_double/double_conversion.h [new file with mode: 0644]
example_avr_double/doubleproto.proto [new file with mode: 0644]
example_avr_double/encode_double.c [new file with mode: 0644]
example_avr_double/test_conversions.c [new file with mode: 0644]

diff --git a/example_avr_double/Makefile b/example_avr_double/Makefile
new file mode 100644 (file)
index 0000000..74300fc
--- /dev/null
@@ -0,0 +1,22 @@
+CFLAGS=-Wall -Werror -I .. -g -O0
+DEPS=double_conversion.c ../pb_decode.c ../pb_decode.h ../pb_encode.c ../pb_encode.h ../pb.h
+
+all: run_tests
+
+clean:
+       rm -f test_conversions encode_double decode_double doubleproto.pb.c doubleproto.pb.h
+
+test_conversions: test_conversions.c double_conversion.c
+       $(CC) $(CFLAGS) -o $@ $^
+
+%: %.c $(DEPS) doubleproto.pb.h doubleproto.pb.c
+       $(CC) $(CFLAGS) -o $@ $< double_conversion.c ../pb_decode.c ../pb_encode.c doubleproto.pb.c
+
+doubleproto.pb.c doubleproto.pb.h: doubleproto.proto ../generator/nanopb_generator.py
+       protoc -I. -I../generator -I/usr/include -odoubleproto.pb $<
+       python ../generator/nanopb_generator.py doubleproto.pb
+
+run_tests: test_conversions encode_double decode_double
+       ./test_conversions
+       ./encode_double | ./decode_double
+    
diff --git a/example_avr_double/README.txt b/example_avr_double/README.txt
new file mode 100644 (file)
index 0000000..0090d72
--- /dev/null
@@ -0,0 +1,22 @@
+Some processors/compilers, such as AVR-GCC, do not support the double
+datatype. Instead, they have sizeof(double) == 4. Because protocol
+binary format uses the double encoding directly, this causes trouble
+if the protocol in .proto requires double fields.
+
+This directory contains a solution to this problem. It uses uint64_t
+to store the raw wire values, because its size is correct on all
+platforms. The file double_conversion.c provides functions that
+convert these values to/from floats, without relying on compiler
+support.
+
+To use this method, you need to make two modifications to your code:
+
+1) Change all 'double' fields into 'fixed64' in the .proto.
+
+2) Whenever writing to a 'double' field, use float_to_double().
+
+3) Whenever reading a 'double' field, use double_to_float().
+
+The conversion routines should be as accurate as the float datatype can
+be. Furthermore, they should handle all special values (NaN, inf, denormalized
+numbers) correctly. There are testcases in test_conversions.c.
diff --git a/example_avr_double/decode_double.c b/example_avr_double/decode_double.c
new file mode 100644 (file)
index 0000000..5802eca
--- /dev/null
@@ -0,0 +1,33 @@
+/* Decodes a double value into a float variable.
+ * Used to read double values with AVR code, which doesn't support double directly.
+ */
+
+#include <stdio.h>
+#include <pb_decode.h>
+#include "double_conversion.h"
+#include "doubleproto.pb.h"
+
+int main()
+{
+    uint8_t buffer[32];
+    size_t count = fread(buffer, 1, sizeof(buffer), stdin);
+    pb_istream_t stream = pb_istream_from_buffer(buffer, count);
+    
+    AVRDoubleMessage message;
+    pb_decode(&stream, AVRDoubleMessage_fields, &message);
+    
+    float v1 = double_to_float(message.field1);
+    float v2 = double_to_float(message.field2);
+
+    printf("Values: %f %f\n", v1, v2);
+    
+    if (v1 == 1234.5678f &&
+        v2 == 0.00001f)
+    {
+        return 0;
+    }
+    else
+    {
+        return 1;
+    }
+}
diff --git a/example_avr_double/double_conversion.c b/example_avr_double/double_conversion.c
new file mode 100644 (file)
index 0000000..cf79b9a
--- /dev/null
@@ -0,0 +1,123 @@
+/* Conversion routines for platforms that do not support 'double' directly. */
+
+#include "double_conversion.h"
+#include <math.h>
+
+typedef union {
+    float f;
+    uint32_t i;
+} conversion_t;
+
+/* Note: IEE 754 standard specifies float formats as follows:
+ * Single precision: sign,  8-bit exp, 23-bit frac.
+ * Double precision: sign, 11-bit exp, 52-bit frac.
+ */
+
+uint64_t float_to_double(float value)
+{
+    conversion_t in;
+    in.f = value;
+    uint8_t sign;
+    int16_t exponent;
+    uint64_t mantissa;
+    
+    /* Decompose input value */
+    sign = (in.i >> 31) & 1;
+    exponent = ((in.i >> 23) & 0xFF) - 127;
+    mantissa = in.i & 0x7FFFFF;
+    
+    if (exponent == 128)
+    {
+        /* Special value (NaN etc.) */
+        exponent = 1024;
+    }
+    else if (exponent == -127)
+    {
+        if (!mantissa)
+        {
+            /* Zero */
+            exponent = -1023;
+        }
+        else
+        {
+            /* Denormalized */
+            mantissa <<= 1;
+            while (!(mantissa & 0x800000))
+            {
+                mantissa <<= 1;
+                exponent--;
+            }
+            mantissa &= 0x7FFFFF;
+        }
+    }
+    
+    /* Combine fields */
+    mantissa <<= 29;
+    mantissa |= (uint64_t)(exponent + 1023) << 52;
+    mantissa |= (uint64_t)sign << 63;
+    
+    return mantissa;
+}
+
+float double_to_float(uint64_t value)
+{
+    uint8_t sign;
+    int16_t exponent;
+    uint32_t mantissa;
+    conversion_t out;
+
+    /* Decompose input value */
+    sign = (value >> 63) & 1;
+    exponent = ((value >> 52) & 0x7FF) - 1023;
+    mantissa = (value >> 28) & 0xFFFFFF; /* Highest 24 bits */
+    /* Figure if value is in range representable by floats. */
+    if (exponent == 1024)
+    {
+        /* Special value */
+        exponent = 128;
+    }
+    else if (exponent > 127)
+    {
+        /* Too large */        
+        if (sign)
+            return -INFINITY;
+        else
+            return INFINITY;
+    }
+    else if (exponent < -150)
+    {
+        /* Too small */
+        if (sign)
+            return -0.0f;
+        else
+            return 0.0f;
+    }
+    else if (exponent < -126)
+    {
+        /* Denormalized */
+        mantissa |= 0x1000000;
+        mantissa >>= (-126 - exponent);
+        exponent = -127;
+    }
+    /* Round off mantissa */
+    mantissa = (mantissa + 1) >> 1;
+    
+    /* Check if mantissa went over 2.0 */
+    if (mantissa & 0x800000)
+    {
+        exponent += 1;
+        mantissa &= 0x7FFFFF;
+        mantissa >>= 1;
+    }
+    
+    /* Combine fields */
+    out.i = mantissa;
+    out.i |= (uint32_t)(exponent + 127) << 23;
+    out.i |= (uint32_t)sign << 31;
+    
+    return out.f;
+}
+
+
diff --git a/example_avr_double/double_conversion.h b/example_avr_double/double_conversion.h
new file mode 100644 (file)
index 0000000..62b6a8a
--- /dev/null
@@ -0,0 +1,26 @@
+/* AVR-GCC does not have real double datatype. Instead its double
+ * is equal to float, i.e. 32 bit value. If you need to communicate
+ * with other systems that use double in their .proto files, you
+ * need to do some conversion.
+ *
+ * These functions use bitwise operations to mangle floats into doubles
+ * and then store them in uint64_t datatype.
+ */
+
+#ifndef DOUBLE_CONVERSION
+#define DOUBLE_CONVERSION
+
+#include <stdint.h>
+
+/* Convert native 4-byte float into a 8-byte double. */
+extern uint64_t float_to_double(float value);
+
+/* Convert 8-byte double into native 4-byte float.
+ * Values are rounded to nearest, 0.5 away from zero.
+ * Overflowing values are converted to Inf or -Inf.
+ */
+extern float double_to_float(uint64_t value);
+
+
+#endif
+
diff --git a/example_avr_double/doubleproto.proto b/example_avr_double/doubleproto.proto
new file mode 100644 (file)
index 0000000..d8b7f2d
--- /dev/null
@@ -0,0 +1,13 @@
+// A message containing doubles, as used by other applications.
+message DoubleMessage {
+    required double field1 = 1;
+    required double field2 = 2;
+}
+
+// A message containing doubles, but redefined using uint64_t.
+// For use in AVR code.
+message AVRDoubleMessage {
+    required fixed64 field1 = 1;
+    required fixed64 field2 = 2;
+}
+
diff --git a/example_avr_double/encode_double.c b/example_avr_double/encode_double.c
new file mode 100644 (file)
index 0000000..cd532d4
--- /dev/null
@@ -0,0 +1,25 @@
+/* Encodes a float value into a double on the wire.
+ * Used to emit doubles from AVR code, which doesn't support double directly.
+ */
+
+#include <stdio.h>
+#include <pb_encode.h>
+#include "double_conversion.h"
+#include "doubleproto.pb.h"
+
+int main()
+{
+    AVRDoubleMessage message = {
+        float_to_double(1234.5678f),
+        float_to_double(0.00001f)
+    };
+    
+    uint8_t buffer[32];
+    pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
+    
+    pb_encode(&stream, AVRDoubleMessage_fields, &message);
+    fwrite(buffer, 1, stream.bytes_written, stdout);
+
+    return 0;
+}
+
diff --git a/example_avr_double/test_conversions.c b/example_avr_double/test_conversions.c
new file mode 100644 (file)
index 0000000..22620a6
--- /dev/null
@@ -0,0 +1,56 @@
+#include "double_conversion.h"
+#include <math.h>
+#include <stdio.h>
+
+static const double testvalues[] = {
+           0.0,        -0.0,         0.1,         -0.1,
+          M_PI,       -M_PI,  123456.789,  -123456.789,
+      INFINITY,   -INFINITY,         NAN, INFINITY - INFINITY,
+          1e38,       -1e38,        1e39,        -1e39,
+         1e-38,      -1e-38,       1e-39,       -1e-39,
+   3.14159e-37,-3.14159e-37, 3.14159e-43, -3.14159e-43,
+         1e-60,      -1e-60,       1e-45,       -1e-45,
+    0.99999999999999, -0.99999999999999, 127.999999999999, -127.999999999999
+};
+
+#define TESTVALUES_COUNT (sizeof(testvalues)/sizeof(testvalues[0]))
+
+int main()
+{
+    int status = 0;
+    int i;
+    for (i = 0; i < TESTVALUES_COUNT; i++)
+    {
+        double orig = testvalues[i];
+        float expected_float = (float)orig;
+        double expected_double = (double)expected_float;
+        
+        float got_float = double_to_float(*(uint64_t*)&orig);
+        uint64_t got_double = float_to_double(got_float);
+        
+        uint32_t e1 = *(uint32_t*)&expected_float;
+        uint32_t g1 = *(uint32_t*)&got_float;
+        uint64_t e2 = *(uint64_t*)&expected_double;
+        uint64_t g2 = got_double;
+        
+        if (g1 != e1)
+        {
+            printf("%3d double_to_float fail: %08x != %08x\n", i, g1, e1);
+            status = 1;
+        }
+        
+        if (g2 != e2)
+        {
+            printf("%3d float_to_double fail: %016llx != %016llx\n", i,
+                (unsigned long long)g2,
+                (unsigned long long)e2);
+            status = 1;
+        }
+    }
+
+    return status;
+}
+
+    
+    
+